Update the Language Server API (#3308)

This commit is contained in:
Dmitry Bushev 2022-03-03 16:28:04 +03:00 committed by GitHub
parent 500aed9d86
commit 40f44be858
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 480 additions and 302 deletions

View File

@ -1,4 +1,4 @@
name: Base
name: Searcher
namespace: Standard
version: 0.0.0-dev
license: APLv2

View File

@ -62,12 +62,9 @@ transport formats, please look [here](./protocol-architecture).
- [`LibraryVersion`](#libraryversion)
- [`Contact`](#contact)
- [`EditionReference`](#editionreference)
- [`ComponentGroups`](#componentgroups)
- [`ComponentGroup`](#componentgroup)
- [`ExtendedComponentGroup`](#extendedcomponentgroup)
- [`ModuleReference`](#modulereference)
- [`Component`](#component)
- [`LibraryComponentGroups`](#librarycomponentgroups)
- [`LibraryComponentGroup`](#librarycomponentgroup)
- [`LibraryComponent`](#librarycomponent)
- [Connection Management](#connection-management)
- [`session/initProtocolConnection`](#sessioninitprotocolconnection)
- [`session/initBinaryConnection`](#sessioninitbinaryconnection)
@ -1427,84 +1424,18 @@ interface NamedEdition {
}
```
### `ComponentGroups`
### `LibraryComponentGroups`
The description of component groups provided by the package. Object fields can
be omitted if the corresponding list is empty.
```typescript
interface ComponentGroups {
interface LibraryComponentGroups {
/** The list of component groups provided by the package. */
newGroups?: ComponentGroup[];
newGroups?: LibraryComponentGroup[];
/** The list of component groups that this package extends.*/
extendedGroups?: ExtendedComponentGroup[];
}
```
### `ComponentGroup`
The definition of a single component group.
```typescript
interface ComponentGroup {
/** The module name containing the declared componennts. */
module: string;
color?: string;
icon?: string;
/** The list of components provided by this component group. */
exports: Component[];
}
```
### `ExtendedComponentGroup`
The definition of a component group that extends an existing one.
```typescript
interface ExtendedComponentGroup {
/** The reference to the component group module being extended. */
module: ModuleReference;
/** The list of components provided by this component group. */
exports: Component[];
}
```
### `ModuleReference`
The reference to a module.
```typescript
interface ModuleReference {
/**
* A string consisting of a namespace and a lirary name separated by the dot
* <namespace>.<library name>, i.e. `Standard.Base`.
*/
libraryName: string;
/** The module name without the library name prefix.
* E.g. given the `Standard.Base.Data.Vector` module reference,
* the `moduleName` field contains `Data.Vector`.
*/
moduleName: string;
}
```
### `Component`
A single component of a component group.
```typescript
interface Component {
/** The component name. */
name: string;
/** The component shortcut. */
shortcut?: string;
extendedGroups?: LibraryComponentGroup[];
}
```
@ -1515,8 +1446,9 @@ The component group provided by a library.
```typescript
interface LibraryComponentGroup {
/**
* A string consisting of a namespace and a lirary name separated by the dot
* <namespace>.<library name>, i.e. `Standard.Base`.
* Thf fully qualified module name. A string consisting of a namespace and
* a library name separated by the dot <namespace>.<library name>,
* i.e. `Standard.Base`.
*/
library: string;
@ -1531,7 +1463,21 @@ interface LibraryComponentGroup {
icon?: string;
/** The list of components provided by this component group. */
exports: Component[];
exports: LibraryComponent[];
}
```
### `LibraryComponent`
A single component of a component group.
```typescript
interface LibraryComponent {
/** The component name. */
name: string;
/** The component shortcut. */
shortcut?: string;
}
```
@ -4620,7 +4566,7 @@ All returned fields are optional, as they may be missing.
```typescript
{
license?: String;
componentGroups?: ComponentGroups;
componentGroups?: LibraryComponentGroups;
}
```

View File

@ -2,7 +2,6 @@ package org.enso.languageserver.libraries
import org.enso.editions.LibraryName
import org.enso.pkg.{
ComponentGroup,
ComponentGroups,
Config,
ExtendedComponentGroup,
@ -53,7 +52,9 @@ final class ComponentGroupsResolver {
val newLibraryComponentGroups: View[LibraryComponentGroup] =
libraryComponents.view
.flatMap { case (libraryName, componentGroups) =>
componentGroups.newGroups.map(toLibraryComponentGroup(libraryName, _))
componentGroups.newGroups.map(
LibraryComponentGroup.fromComponentGroup(libraryName, _)
)
}
val newLibraryComponentGroupsMap
: Map[ModuleReference, LibraryComponentGroup] =
@ -114,29 +115,9 @@ final class ComponentGroupsResolver {
extendedComponentGroup: ExtendedComponentGroup
): LibraryComponentGroup =
libraryComponentGroup.copy(
exports = libraryComponentGroup.exports :++ extendedComponentGroup.exports
exports = libraryComponentGroup.exports :++
extendedComponentGroup.exports.map(LibraryComponent.fromComponent)
)
/** Convert [[ComponentGroup]] to [[LibraryComponentGroup]] representation.
*
* @param libraryName the library name defining this component group
* @param componentGroup the component group to convert
* @return the [[LibraryComponentGroup]] representation of this component
* group
*/
private def toLibraryComponentGroup(
libraryName: LibraryName,
componentGroup: ComponentGroup
): LibraryComponentGroup = {
LibraryComponentGroup(
libraryName,
componentGroup.module,
componentGroup.color,
componentGroup.icon,
componentGroup.exports
)
}
}
object ComponentGroupsResolver {

View File

@ -1,10 +1,10 @@
package org.enso.languageserver.libraries
import io.circe.{Json, JsonObject}
import io.circe.literal.JsonStringContext
import io.circe.{Json, JsonObject}
import org.enso.editions.{LibraryName, LibraryVersion}
import org.enso.jsonrpc.{Error, HasParams, HasResult, Method, Unused}
import org.enso.pkg.{ComponentGroups, Contact}
import org.enso.pkg.Contact
object LibraryApi {
case object EditionsListAvailable extends Method("editions/listAvailable") {
@ -190,7 +190,7 @@ object LibraryApi {
// should be removed when the integration with IDE is finished
case class Result(
license: Option[String],
componentGroups: Option[ComponentGroups],
componentGroups: Option[LibraryComponentGroups],
raw: JsonObject
)

View File

@ -3,9 +3,77 @@ package org.enso.languageserver.libraries
import io.circe._
import io.circe.syntax._
import org.enso.editions.LibraryName
import org.enso.pkg.{Component, ModuleName}
import org.enso.pkg.{
Component,
ComponentGroup,
ComponentGroups,
ExtendedComponentGroup,
ModuleName,
Shortcut
}
/** The component group definition of a library.
/** The description of component groups provided by the package. This
* representation is used in the JSONRPC API.
*
* @param newGroups the list of component groups provided by the package
* @param extendedGroups the list of component groups that this package extends
*/
case class LibraryComponentGroups(
newGroups: List[LibraryComponentGroup],
extendedGroups: List[LibraryComponentGroup]
)
object LibraryComponentGroups {
/** Create a [[LibraryComponentGroups]] from the provided [[ComponentGroups]].
*
* @param libraryName the library name defining these component groups
* @param componentGroups the component groups to convert
* @return the [[LibraryComponentGroups]] representation of the provided
* component groups
*/
def fromComponentGroups(
libraryName: LibraryName,
componentGroups: ComponentGroups
): LibraryComponentGroups =
LibraryComponentGroups(
componentGroups.newGroups.map(
LibraryComponentGroup.fromComponentGroup(libraryName, _)
),
componentGroups.extendedGroups.map(
LibraryComponentGroup.fromExtendedComponentGroup
)
)
/** Fields for use when serializing the [[LibraryComponentGroups]]. */
private object Fields {
val newGroups = "newGroups"
val extendedGroups = "extendedGroups"
}
/** [[Encoder]] instance for the [[LibraryComponentGroups]]. */
implicit val encoder: Encoder[LibraryComponentGroups] = { componentGroups =>
val newGroups = Option.unless(componentGroups.newGroups.isEmpty)(
Fields.newGroups -> componentGroups.newGroups.asJson
)
val extendedGroups = Option.unless(componentGroups.extendedGroups.isEmpty)(
Fields.extendedGroups -> componentGroups.extendedGroups.asJson
)
Json.obj(newGroups.toSeq ++ extendedGroups.toSeq: _*)
}
/** [[Decoder]] instance for the [[LibraryComponentGroups]]. */
implicit val decoder: Decoder[LibraryComponentGroups] = { json =>
for {
newGroups <- json
.getOrElse[List[LibraryComponentGroup]](Fields.newGroups)(List())
extendedGroups <- json
.getOrElse[List[LibraryComponentGroup]](Fields.extendedGroups)(List())
} yield LibraryComponentGroups(newGroups, extendedGroups)
}
}
/** The component group definition of a library. This representation is used in
* the JSONRPC API.
*
* @param library the library name
* @param module the module name
@ -18,10 +86,47 @@ case class LibraryComponentGroup(
module: ModuleName,
color: Option[String],
icon: Option[String],
exports: Seq[Component]
exports: Seq[LibraryComponent]
)
object LibraryComponentGroup {
/** create a [[LibraryComponentGroup]] from the provided [[ComponentGroup]].
*
* @param libraryName the library name defining this component group
* @param componentGroup the component group to convert
* @return the [[LibraryComponentGroup]] representation of this component
* group
*/
def fromComponentGroup(
libraryName: LibraryName,
componentGroup: ComponentGroup
): LibraryComponentGroup =
LibraryComponentGroup(
library = libraryName,
module = componentGroup.module,
color = componentGroup.color,
icon = componentGroup.icon,
exports = componentGroup.exports.map(LibraryComponent.fromComponent)
)
/** Create a [[LibraryComponentGroup]] from the [[ExtendedComponentGroup]].
*
* @param extendedComponentGroup the extended component group to convert
* @return the [[LibraryComponentGroup]] representation of the provided
* extended component group
*/
def fromExtendedComponentGroup(
extendedComponentGroup: ExtendedComponentGroup
): LibraryComponentGroup =
LibraryComponentGroup(
library = extendedComponentGroup.module.libraryName,
module = extendedComponentGroup.module.moduleName,
color = None,
icon = None,
exports =
extendedComponentGroup.exports.map(LibraryComponent.fromComponent)
)
/** Fields for use when serializing the [[LibraryComponentGroup]]. */
private object Fields {
val Library = "library"
@ -52,8 +157,47 @@ object LibraryComponentGroup {
module <- json.get[ModuleName](Fields.Module)
color <- json.get[Option[String]](Fields.Color)
icon <- json.get[Option[String]](Fields.Icon)
exports <- json.getOrElse[List[Component]](Fields.Exports)(List())
exports <- json.getOrElse[List[LibraryComponent]](Fields.Exports)(List())
} yield LibraryComponentGroup(library, module, color, icon, exports)
}
}
/** A single component of a component group. This representation is used in
* the JSONRPC API.
*
* @param name the component name
* @param shortcut the component shortcut
*/
case class LibraryComponent(name: String, shortcut: Option[Shortcut])
object LibraryComponent {
/** Create a [[LibraryComponent]] from the provided [[Component]].
*
* @param component the component to convert
* @return the [[LibraryComponent]] representation of this component
*/
def fromComponent(component: Component): LibraryComponent =
LibraryComponent(component.name, component.shortcut)
object Fields {
val Name = "name"
val Shortcut = "shortcut"
}
/** [[Encoder]] instance for the [[LibraryComponent]]. */
implicit val encoder: Encoder[LibraryComponent] = { component =>
val shortcut = component.shortcut.map(Fields.Shortcut -> _.asJson)
Json.obj(
Seq(Fields.Name -> component.name.asJson) ++
shortcut.toSeq: _*
)
}
/** [[Decoder]] instance for the [[LibraryComponent]]. */
implicit val decoder: Decoder[LibraryComponent] = { json =>
for {
name <- json.get[String](Fields.Name)
shortcut <- json.getOrElse[Option[Shortcut]](Fields.Shortcut)(None)
} yield LibraryComponent(name, shortcut)
}
}

View File

@ -202,12 +202,16 @@ class LocalLibraryManager(
configPath = libraryRootPath / Package.configFileName
config <- loadPackageConfig(configPath)
} yield {
if (config.componentGroups.isLeft) {
logger.error(
s"Failed to parse library [$libraryName] component groups."
)
config.componentGroups match {
case Left(error) =>
logger.error(
s"Failed to parse library [$libraryName] component groups " +
s"(reason: ${error.message})."
)
case _ =>
}
GetPackageResponse(
libraryName = LibraryName(config.namespace, config.name),
license = config.license,
componentGroups = config.componentGroups.toOption,
rawPackage = config.originalJson

View File

@ -1,6 +1,8 @@
package org.enso.languageserver.libraries
import org.enso.editions.provider.EditionNotFound
import org.enso.jsonrpc
import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError
/** The object mapping the [[LocalLibraryManagerProtocol]] failures into the
* corresponding JSONRPC error messages.
@ -18,4 +20,20 @@ object LocalLibraryManagerFailureMapper {
case LocalLibraryManagerProtocol.InvalidSemverVersionError(version) =>
LibraryApi.InvalidSemverVersion(version)
}
/** Convert the exceptions raised in the library management api to the
* corresponding JSONRPC errors.
*
* @param error the raised exception
* @return the JSONRPC error message
*/
def mapException(error: Throwable): jsonrpc.Error =
error match {
case ex: LocalLibraryManagerProtocol.LocalLibraryNotFoundError =>
LibraryApi.LocalLibraryNotFound(ex.libraryName)
case ex: EditionNotFound =>
LibraryApi.EditionNotFoundError(ex.editionName)
case _ =>
FileSystemError(error.getMessage)
}
}

View File

@ -31,6 +31,7 @@ object LocalLibraryManagerProtocol {
/** A response to the [[GetPackage]] request. */
case class GetPackageResponse(
libraryName: LibraryName,
license: String,
componentGroups: Option[ComponentGroups],
rawPackage: JsonObject

View File

@ -5,8 +5,10 @@ import akka.pattern.pipe
import com.typesafe.scalalogging.LazyLogging
import org.enso.editions.updater.EditionManager
import org.enso.jsonrpc.{Id, Request, ResponseError, ResponseResult}
import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError
import org.enso.languageserver.libraries.BlockingOperation
import org.enso.languageserver.libraries.{
BlockingOperation,
LocalLibraryManagerFailureMapper
}
import org.enso.languageserver.libraries.LibraryApi._
import org.enso.languageserver.util.UnhandledLogging
@ -49,7 +51,7 @@ class EditionsListAvailableHandler(editionManager: EditionManager)
case Status.Failure(exception) =>
replyTo ! ResponseError(
Some(id),
FileSystemError(exception.toString)
LocalLibraryManagerFailureMapper.mapException(exception)
)
context.stop(self)
}

View File

@ -5,14 +5,14 @@ import akka.pattern.pipe
import com.typesafe.scalalogging.LazyLogging
import org.enso.editions.{LibraryName, LibraryVersion}
import org.enso.jsonrpc.{Id, Request, ResponseError, ResponseResult}
import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError
import org.enso.languageserver.libraries.LibraryApi._
import org.enso.languageserver.libraries.{
BlockingOperation,
ComponentGroupsResolver,
ComponentGroupsValidator,
EditionReferenceResolver,
LibraryComponentGroup
LibraryComponentGroup,
LocalLibraryManagerFailureMapper
}
import org.enso.languageserver.util.UnhandledLogging
import org.enso.librarymanager.local.LocalLibraryProvider
@ -50,7 +50,7 @@ class EditionsListDefinedComponentsHandler(
BlockingOperation
.run {
val edition = editionReferenceResolver.resolveEdition(reference).get
val definedLibraries = edition.getAllDefinedLibraries.view
val definedLibraries = edition.getAllDefinedLibraries.toSeq
.map { case (name, version) =>
readLocalPackage(name, version)
}
@ -109,7 +109,7 @@ class EditionsListDefinedComponentsHandler(
case Status.Failure(exception) =>
replyTo ! ResponseError(
Some(id),
FileSystemError(exception.getMessage)
LocalLibraryManagerFailureMapper.mapException(exception)
)
context.stop(self)
}

View File

@ -5,12 +5,12 @@ import akka.pattern.pipe
import com.typesafe.scalalogging.LazyLogging
import org.enso.editions.LibraryVersion
import org.enso.jsonrpc.{Id, Request, ResponseError, ResponseResult}
import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError
import org.enso.languageserver.libraries.LibraryApi._
import org.enso.languageserver.libraries.{
BlockingOperation,
EditionReferenceResolver,
LibraryEntry
LibraryEntry,
LocalLibraryManagerFailureMapper
}
import org.enso.languageserver.util.UnhandledLogging
import org.enso.librarymanager.local.LocalLibraryProvider
@ -74,10 +74,9 @@ class EditionsListDefinedLibrariesHandler(
context.stop(self)
case Status.Failure(exception) =>
// TODO [RW] more detailed errors
replyTo ! ResponseError(
Some(id),
FileSystemError(exception.getMessage)
LocalLibraryManagerFailureMapper.mapException(exception)
)
context.stop(self)
}

View File

@ -9,7 +9,6 @@ import nl.gn0s1s.bump.SemVer
import org.enso.editions.Editions.Repository
import org.enso.editions.LibraryName
import org.enso.jsonrpc._
import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError
import org.enso.languageserver.libraries.LibraryApi._
import org.enso.languageserver.libraries.{
LibraryEntry,
@ -101,7 +100,10 @@ class LibraryGetMetadataHandler(
context.stop(self)
case Status.Failure(exception) =>
replyTo ! ResponseError(Some(id), FileSystemError(exception.getMessage))
replyTo ! ResponseError(
Some(id),
LocalLibraryManagerFailureMapper.mapException(exception)
)
cancellable.cancel()
context.stop(self)
}

View File

@ -7,13 +7,8 @@ import nl.gn0s1s.bump.SemVer
import org.enso.editions.Editions.Repository
import org.enso.editions.LibraryName
import org.enso.jsonrpc._
import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError
import org.enso.languageserver.libraries.LibraryApi._
import org.enso.languageserver.libraries.{
LibraryEntry,
LocalLibraryManagerFailureMapper,
LocalLibraryManagerProtocol
}
import org.enso.languageserver.libraries._
import org.enso.languageserver.requesthandler.RequestTimeout
import org.enso.languageserver.util.UnhandledLogging
import org.enso.librarymanager.published.PublishedLibraryCache
@ -83,6 +78,7 @@ class LibraryGetPackageHandler(
context.stop(self)
case LocalLibraryManagerProtocol.GetPackageResponse(
libraryName,
license,
componentGroups,
rawPackage
@ -92,7 +88,11 @@ class LibraryGetPackageHandler(
id,
LibraryGetPackage.Result(
Option.unless(license.isEmpty)(license),
componentGroups,
componentGroups.flatMap { groups =>
Option.unless(
groups.newGroups.isEmpty && groups.extendedGroups.isEmpty
)(LibraryComponentGroups.fromComponentGroups(libraryName, groups))
},
rawPackage
)
)
@ -105,7 +105,10 @@ class LibraryGetPackageHandler(
context.stop(self)
case Status.Failure(exception) =>
replyTo ! ResponseError(Some(id), FileSystemError(exception.getMessage))
replyTo ! ResponseError(
Some(id),
LocalLibraryManagerFailureMapper.mapException(exception)
)
cancellable.cancel()
context.stop(self)
}
@ -133,6 +136,7 @@ class LibraryGetPackageHandler(
.readPackage()
.map(config =>
LocalLibraryManagerProtocol.GetPackageResponse(
LibraryName(config.namespace, config.name),
config.license,
config.componentGroups.toOption,
config.originalJson
@ -150,6 +154,7 @@ class LibraryGetPackageHandler(
.fetchPackageConfig()
.toFuture
} yield LocalLibraryManagerProtocol.GetPackageResponse(
libraryName = LibraryName(config.namespace, config.name),
license = config.license,
componentGroups = config.componentGroups.toOption,
rawPackage = config.originalJson

View File

@ -311,6 +311,6 @@ object ComponentGroupsResolverSpec {
module = ModuleName(module),
color = None,
icon = None,
exports = exports.map(Component(_, None))
exports = exports.map(LibraryComponent(_, None))
)
}

View File

@ -5,7 +5,7 @@ import io.circe.{Json, JsonObject}
import nl.gn0s1s.bump.SemVer
import org.enso.distribution.FileSystem
import org.enso.editions.{Editions, LibraryName}
import org.enso.languageserver.libraries.LibraryEntry
import org.enso.languageserver.libraries.{LibraryComponentGroups, LibraryEntry}
import org.enso.languageserver.libraries.LibraryEntry.PublishedLibraryVersion
import org.enso.librarymanager.published.bundles.LocalReadOnlyRepository
import org.enso.librarymanager.published.repository.{
@ -227,6 +227,32 @@ class LibrariesTest extends BaseServerTest {
""")
}
"return LibraryNotFound error when getting the metadata of unknown library" in {
val client = getInitialisedWsClient()
client.send(json"""
{ "jsonrpc": "2.0",
"method": "library/getMetadata",
"id": 0,
"params": {
"namespace": "user",
"name": "Get_Package_Unknown",
"version": {
"type": "LocalLibraryVersion"
}
}
}
""")
client.expectJson(json"""
{ "jsonrpc": "2.0",
"id": 0,
"error": {
"code": 8007,
"message": "Local library [user.Get_Package_Unknown] has not been found."
}
}
""")
}
"get the package config" in {
val client = getInitialisedWsClient()
val testComponentGroups = ComponentGroups(
@ -313,8 +339,37 @@ class LibrariesTest extends BaseServerTest {
response.hcursor
.downField("result")
.downField("componentGroups")
.as[ComponentGroups]
.rightValue shouldEqual testComponentGroups
.as[LibraryComponentGroups]
.rightValue shouldEqual LibraryComponentGroups.fromComponentGroups(
LibraryName("user", "Get_Package_Test_Lib"),
testComponentGroups
)
}
"return LibraryNotFound error when getting the package of unknown library" in {
val client = getInitialisedWsClient()
client.send(json"""
{ "jsonrpc": "2.0",
"method": "library/getPackage",
"id": 0,
"params": {
"namespace": "user",
"name": "Get_Package_Unknown",
"version": {
"type": "LocalLibraryVersion"
}
}
}
""")
client.expectJson(json"""
{ "jsonrpc": "2.0",
"id": 0,
"error": {
"code": 8007,
"message": "Local library [user.Get_Package_Unknown] has not been found."
}
}
""")
}
"create, publish a library and fetch its manifest from the server" in {

View File

@ -33,12 +33,12 @@ class UpdatingEditionProvider(
name: String
): Either[EditionLoadingError, Editions.Raw.Edition] = {
provider.findEditionForName(name) match {
case Left(EditionNotFound()) =>
case Left(EditionNotFound(_)) =>
if (!wasUpdateTried) {
wasUpdateTried = true
updater.updateEditions()
provider.findEditionForName(name)
} else Left(EditionNotFound())
} else Left(EditionNotFound(name))
case Left(otherError) => Left(otherError)
case Right(value) => Right(value)
}

View File

@ -5,7 +5,7 @@ sealed class EditionLoadingError(message: String, cause: Throwable = null)
extends RuntimeException(message, cause)
/** Indicates that the requested edition was not found in the caches. */
case class EditionNotFound()
case class EditionNotFound(editionName: String)
extends EditionLoadingError("The edition was not found.")
/** Indicates that the edition was found but could not be read, for example due

View File

@ -28,11 +28,11 @@ class FileSystemEditionProvider(searchPaths: List[Path])
case head :: tail =>
val headResult = loadEdition(name, head)
headResult match {
case Left(EditionNotFound()) =>
case Left(EditionNotFound(_)) =>
findEdition(name, tail)
case _ => headResult
}
case Nil => Left(EditionNotFound())
case Nil => Left(EditionNotFound(name))
}
private def loadEdition(
@ -47,7 +47,7 @@ class FileSystemEditionProvider(searchPaths: List[Path])
.toEither
.left
.map(EditionReadError)
} else Left(EditionNotFound())
} else Left(EditionNotFound(name))
}
/** Finds all editions available on the [[searchPaths]]. */

View File

@ -52,7 +52,7 @@ class EditionResolverSpec
override def findEditionForName(
name: String
): Either[EditionLoadingError, Editions.Raw.Edition] =
editions.get(name).toRight(EditionNotFound())
editions.get(name).toRight(EditionNotFound(name))
override def findAvailableEditions(): Seq[String] = editions.keys.toSeq
}

View File

@ -63,7 +63,6 @@ object ComponentGroup {
/** Fields for use when serializing the [[ComponentGroup]]. */
private object Fields {
val Module = "module"
val Color = "color"
val Icon = "icon"
val Exports = "exports"
@ -77,26 +76,45 @@ object ComponentGroup {
Fields.Exports -> componentGroup.exports.asJson
)
Json.obj(
(Fields.Module -> componentGroup.module.asJson) +:
(color.toSeq ++ icon.toSeq ++ exports.toSeq): _*
componentGroup.module.name -> Json.obj(
color.toSeq ++ icon.toSeq ++ exports.toSeq: _*
)
)
}
/** [[Decoder]] instance for the [[ComponentGroup]]. */
implicit val decoder: Decoder[ComponentGroup] = { json =>
for {
name <- ConfigCodecs
.getFromObject[ModuleName](
"component group name",
Fields.Module,
json
)
color <- json.get[Option[String]](Fields.Color)
icon <- json.get[Option[String]](Fields.Icon)
exports <- json.getOrElse[List[Component]](Fields.Exports)(List())
} yield ComponentGroup(name, color, icon, exports)
name <- decodeName(json)
componentGroup <- decodeComponentGroup(
ModuleName(name),
json.downField(name)
)
} yield componentGroup
}
private def decodeName(cursor: ACursor): Decoder.Result[String] =
ConfigCodecs
.getNameFromKey(cursor)
.toRight(decodingFailure(cursor.history))
private def decodeComponentGroup(
name: ModuleName,
cursor: ACursor
): Decoder.Result[ComponentGroup] = {
if (cursor.keys.nonEmpty) {
for {
color <- cursor.get[Option[String]](Fields.Color)
icon <- cursor.get[Option[String]](Fields.Icon)
exports <- cursor.getOrElse[List[Component]](Fields.Exports)(List())
} yield ComponentGroup(name, color, icon, exports)
} else {
Left(decodingFailure(cursor.history))
}
}
private def decodingFailure(history: List[CursorOp]): DecodingFailure =
DecodingFailure("Failed to decode component group", history)
}
/** The definition of a component group that extends an existing one.
@ -112,7 +130,6 @@ object ExtendedComponentGroup {
/** Fields for use when serializing the [[ExtendedComponentGroup]]. */
private object Fields {
val Module = "module"
val Exports = "exports"
}
@ -123,22 +140,54 @@ object ExtendedComponentGroup {
Fields.Exports -> extendedComponentGroup.exports.asJson
)
Json.obj(
(Fields.Module -> extendedComponentGroup.module.asJson) +: exports.toSeq: _*
extendedComponentGroup.module.qualifiedName -> Json.obj(
exports.toSeq: _*
)
)
}
/** [[Decoder]] instance for the [[ExtendedComponentGroup]]. */
implicit val decoder: Decoder[ExtendedComponentGroup] = { json =>
for {
reference <- ConfigCodecs
.getFromObject[ModuleReference](
"extended component group reference",
Fields.Module,
json
moduleName <- decodeModuleName(json)
moduleReference <- ModuleReference
.fromModuleName(moduleName)
.toRight(
DecodingFailure(
s"Failed to decode '$moduleName' as a module reference. " +
s"Module reference should consist of a namespace (author), " +
s"library name and a module name (e.g. Standard.Base.Data).",
json.history
)
)
exports <- json.getOrElse[List[Component]](Fields.Exports)(List())
} yield ExtendedComponentGroup(reference, exports)
componentGroup <-
decodeExtendedComponentGroup(
moduleReference,
json.downField(moduleName)
)
} yield componentGroup
}
private def decodeModuleName(cursor: ACursor): Decoder.Result[String] =
ConfigCodecs
.getNameFromKey(cursor)
.toRight(decodingFailure(cursor.history))
private def decodeExtendedComponentGroup(
reference: ModuleReference,
cursor: ACursor
): Decoder.Result[ExtendedComponentGroup] =
if (cursor.keys.nonEmpty) {
for {
exports <- cursor.getOrElse[List[Component]](Fields.Exports)(List())
} yield ExtendedComponentGroup(reference, exports)
} else {
Left(decodingFailure(cursor.history))
}
private def decodingFailure(history: List[CursorOp]): DecodingFailure =
DecodingFailure("Failed to decode extended component group", history)
}
/** A single component of a component group.
@ -150,7 +199,6 @@ case class Component(name: String, shortcut: Option[Shortcut])
object Component {
object Fields {
val Name = "name"
val Shortcut = "shortcut"
}
@ -159,8 +207,9 @@ object Component {
component.shortcut match {
case Some(shortcut) =>
Json.obj(
Fields.Name -> component.name.asJson,
Fields.Shortcut -> shortcut.asJson
component.name -> Json.obj(
Fields.Shortcut -> shortcut.asJson
)
)
case None =>
component.name.asJson
@ -175,11 +224,28 @@ object Component {
case Left(_) =>
for {
name <- ConfigCodecs
.getFromObject[String]("component name", Fields.Name, json)
shortcut <- json.getOrElse[Option[Shortcut]](Fields.Shortcut)(None)
} yield Component(name, shortcut)
.getNameFromKey(json)
.toRight(decodingFailure(json.history))
component <- decodeComponent(name, json.downField(name))
} yield component
}
}
private def decodeComponent(
name: String,
cursor: ACursor
): Decoder.Result[Component] = {
if (cursor.keys.nonEmpty) {
for {
shortcut <- cursor.getOrElse[Option[Shortcut]](Fields.Shortcut)(None)
} yield Component(name, shortcut)
} else {
Left(decodingFailure(cursor.history))
}
}
private def decodingFailure(history: List[CursorOp]): DecodingFailure =
DecodingFailure("Failed to decode exported component", history)
}
/** The shortcut reference to the component.
@ -196,7 +262,12 @@ object Shortcut {
/** [[Decoder]] instance for the [[Shortcut]]. */
implicit val decoder: Decoder[Shortcut] = { json =>
ConfigCodecs.getScalar("shortcut", json).map(Shortcut(_))
ConfigCodecs
.getScalar(json)
.map(Shortcut(_))
.toRight(
DecodingFailure("Failed to decode shortcut", json.history)
)
}
}
@ -222,41 +293,19 @@ case class ModuleReference(
}
object ModuleReference {
private def toModuleString(moduleReference: ModuleReference): String = {
s"${moduleReference.libraryName.namespace}${LibraryName.separator}" +
s"${moduleReference.libraryName.name}${LibraryName.separator}" +
moduleReference.moduleName.name
}
/** [[Encoder]] instance for the [[ModuleReference]]. */
implicit val encoder: Encoder[ModuleReference] = { moduleReference =>
toModuleString(moduleReference).asJson
}
/** [[Decoder]] instance for the [[ModuleReference]]. */
implicit val decoder: Decoder[ModuleReference] = { json =>
json.as[String].flatMap { moduleString =>
moduleString.split(LibraryName.separator).toList match {
case namespace :: name :: module :: modules =>
Right(
ModuleReference(
LibraryName(namespace, name),
ModuleName.fromComponents(module, modules)
)
/** Create a [[ModuleReference]] from string. */
def fromModuleName(moduleName: String): Option[ModuleReference] =
moduleName.split(LibraryName.separator).toList match {
case namespace :: name :: module :: modules =>
Some(
ModuleReference(
LibraryName(namespace, name),
ModuleName.fromComponents(module, modules)
)
case _ =>
Left(
DecodingFailure(
s"Failed to decode '$moduleString' as module reference. " +
s"Module reference should consist of a namespace (author), " +
s"library name and a module name (e.g. Standard.Base.Data).",
json.history
)
)
}
)
case _ =>
None
}
}
}
/** The module name.
@ -266,6 +315,7 @@ object ModuleReference {
case class ModuleName(name: String)
object ModuleName {
/** Create a [[ModuleName]] from its components. */
def fromComponents(item: String, items: List[String]): ModuleName =
ModuleName((item :: items).mkString(LibraryName.separator.toString))

View File

@ -1,74 +1,38 @@
package org.enso.pkg
import io.circe._
import io.circe.syntax._
/** A collection of utility codecs used in the [[Config]]. */
object ConfigCodecs {
/** The common decoding failure.
/** Get the decoded entity name.
*
* @param entity the name of decoded entity
* @param history the list of JSON cursor operations
*/
private def decodingFailure(
entity: String,
history: List[CursorOp]
): DecodingFailure =
DecodingFailure(s"Failed to decode $entity", history)
/** Try to decode the entity `A` from a JSON object.
*
* The entity can be encoded either in the first key of the JSON object with
* the Null value,
* {{{
* { entity: null }
* }}}
*
* or as a value of the provided `keyName` argument
* The entity name is encoded as a key of JSON object.
*
* {{{
* { `keyName`: entity }
* { `name`: entity }
* }}}
*
* @param entity the name of decoded entity that is used in error reporting
* @param keyName the key of the JSON object that contains the entity
* @param cursor the current focus in the JSON document
*/
def getFromObject[A: Decoder](
entity: String,
keyName: String,
cursor: HCursor
): Decoder.Result[A] =
cursor.keys match {
case Some(keys) if keys.nonEmpty =>
cursor
.get[A](keyName)
.orElse {
cursor.get[Json](keys.head).flatMap { json =>
if (json.isNull) {
Decoder[A].decodeJson(keys.head.asJson)
} else {
Left(decodingFailure(entity, cursor.history))
}
}
}
case _ =>
Left(decodingFailure(entity, cursor.history))
def getNameFromKey(cursor: ACursor): Option[String] =
cursor.keys.flatMap {
case keys if keys.size == 1 => keys.headOption
case _ => None
}
/** Get the scalar value of the provided JSON element.
*
* @param entity the name of decoded entity
* @param cursor the current focus in the JSON document
*/
def getScalar(entity: String, cursor: HCursor): Decoder.Result[String] =
def getScalar(cursor: HCursor): Option[String] =
cursor.value.fold(
jsonNull = Left(decodingFailure(entity, cursor.history)),
jsonBoolean = value => Right(value.toString),
jsonNumber = value => Right(value.toString),
jsonString = value => Right(value),
jsonArray = _ => Left(decodingFailure(entity, cursor.history)),
jsonObject = _ => Left(decodingFailure(entity, cursor.history))
jsonNull = None,
jsonBoolean = value => Some(value.toString),
jsonNumber = value => Some(value.toString),
jsonString = value => Some(value),
jsonArray = _ => None,
jsonObject = _ => None
)
}

View File

@ -102,17 +102,19 @@ class ConfigSpec
"""name: FooBar
|component-groups:
| new:
| - Group 1:
| color: '#C047AB'
| icon: icon-name
| exports:
| - foo:
| shortcut: f
| - bar
| - Group 1:
| color: '#C047AB'
| icon: icon-name
| exports:
| - foo:
| shortcut: f
| - bar
| extends:
| - Standard.Base.Group 2:
| exports:
| - bax
| - Standard.Base.Group 2:
| exports:
| - baz:
| shortcut: k
| - quux
|""".stripMargin
val parsed = Config.fromYaml(config).get
@ -134,7 +136,10 @@ class ConfigSpec
LibraryName("Standard", "Base"),
ModuleName("Group 2")
),
exports = List(Component("bax", None))
exports = List(
Component("baz", Some(Shortcut("k"))),
Component("quux", None)
)
)
)
)
@ -144,17 +149,19 @@ class ConfigSpec
serialized should include(
"""component-groups:
| new:
| - module: Group 1
| color: '#C047AB'
| icon: icon-name
| exports:
| - name: foo
| shortcut: f
| - bar
| - Group 1:
| color: '#C047AB'
| icon: icon-name
| exports:
| - foo:
| shortcut: f
| - bar
| extends:
| - module: Standard.Base.Group 2
| exports:
| - bax""".stripMargin.linesIterator.mkString("\n")
| - Standard.Base.Group 2:
| exports:
| - baz:
| shortcut: k
| - quux""".stripMargin.linesIterator.mkString("\n")
)
}
@ -188,15 +195,15 @@ class ConfigSpec
|component-groups:
| extends:
| - Group 1:
| exports:
| - bax
| exports:
| - bax
|""".stripMargin
val parsed = Config.fromYaml(config).get
parsed.componentGroups match {
case Left(f: DecodingFailure) =>
Show[DecodingFailure].show(f) should include(
"Failed to decode 'Group 1' as module reference"
"Failed to decode 'Group 1' as a module reference"
)
case unexpected =>
fail(s"Unexpected result: $unexpected")
@ -209,16 +216,16 @@ class ConfigSpec
|component-groups:
| new:
| - Group 1:
| exports:
| - foo:
| shortcut: f
| - bar:
| shortcut: fgh
| - baz:
| shortcut: 0
| - quux:
| shortcut:
| - hmmm:
| exports:
| - foo:
| shortcut: f
| - bar:
| shortcut: fgh
| - baz:
| shortcut: 0
| - quux:
| shortcut:
| - hmmm
|""".stripMargin
val parsed = Config.fromYaml(config).get
val expectedComponentGroups = ComponentGroups(
@ -248,9 +255,9 @@ class ConfigSpec
|component-groups:
| new:
| - Group 1:
| exports:
| - foo:
| shortcut: []
| exports:
| - foo:
| shortcut: []
|""".stripMargin
val parsed = Config.fromYaml(config).get
parsed.componentGroups match {
@ -275,7 +282,7 @@ class ConfigSpec
parsed.componentGroups match {
case Left(f: DecodingFailure) =>
Show[DecodingFailure].show(f) should include(
"Failed to decode component group name"
"Failed to decode component group"
)
case unexpected =>
fail(s"Unexpected result: $unexpected")
@ -288,14 +295,14 @@ class ConfigSpec
|component-groups:
| new:
| - Group 1:
| exports:
| - one: two
| exports:
| - one: two
|""".stripMargin
val parsed = Config.fromYaml(config).get
parsed.componentGroups match {
case Left(f: DecodingFailure) =>
Show[DecodingFailure].show(f) should include(
"Failed to decode component name"
"Failed to decode exported component"
)
case unexpected =>
fail(s"Unexpected result: $unexpected")