Add API for component groups (#3286)

This commit is contained in:
Dmitry Bushev 2022-02-24 15:41:14 +03:00 committed by GitHub
parent 2ae636f63c
commit 3858ae7517
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 2023 additions and 143 deletions

View File

@ -12,6 +12,9 @@
- Updated to - Updated to
[GraalVM 21.3.0](https://github.com/graalvm/graalvm-ce-builds/releases/tag/vm-21.3.0) [GraalVM 21.3.0](https://github.com/graalvm/graalvm-ce-builds/releases/tag/vm-21.3.0)
([#3258](https://github.com/enso-org/enso/pull/3258)). ([#3258](https://github.com/enso-org/enso/pull/3258)).
- Extended language server API to allow accessing the package definition, and to
get the available component groups
[#3286](https://github.com/enso-org/enso/pull/3286).
## Interpreter/Runtime ## Interpreter/Runtime

View File

@ -62,6 +62,12 @@ transport formats, please look [here](./protocol-architecture).
- [`LibraryVersion`](#libraryversion) - [`LibraryVersion`](#libraryversion)
- [`Contact`](#contact) - [`Contact`](#contact)
- [`EditionReference`](#editionreference) - [`EditionReference`](#editionreference)
- [`ComponentGroups`](#componentgroups)
- [`ComponentGroup`](#componentgroup)
- [`ExtendedComponentGroup`](#extendedcomponentgroup)
- [`ModuleReference`](#modulereference)
- [`Component`](#component)
- [`LibraryComponentGroup`](#librarycomponentgroup)
- [Connection Management](#connection-management) - [Connection Management](#connection-management)
- [`session/initProtocolConnection`](#sessioninitprotocolconnection) - [`session/initProtocolConnection`](#sessioninitprotocolconnection)
- [`session/initBinaryConnection`](#sessioninitbinaryconnection) - [`session/initBinaryConnection`](#sessioninitbinaryconnection)
@ -156,10 +162,12 @@ transport formats, please look [here](./protocol-architecture).
- [`editions/setProjectParentEdition`](#editionssetprojectparentedition) - [`editions/setProjectParentEdition`](#editionssetprojectparentedition)
- [`editions/setProjectLocalLibrariesPreference`](#editionssetprojectlocallibrariespreference) - [`editions/setProjectLocalLibrariesPreference`](#editionssetprojectlocallibrariespreference)
- [`editions/listDefinedLibraries`](#editionslistdefinedlibraries) - [`editions/listDefinedLibraries`](#editionslistdefinedlibraries)
- [`editions/listDefinedComponents`](#editionslistdefinedcomponents)
- [`library/listLocal`](#librarylistlocal) - [`library/listLocal`](#librarylistlocal)
- [`library/create`](#librarycreate) - [`library/create`](#librarycreate)
- [`library/getMetadata`](#librarygetmetadata) - [`library/getMetadata`](#librarygetmetadata)
- [`library/setMetadata`](#librarysetmetadata) - [`library/setMetadata`](#librarysetmetadata)
- [`library/getPackage`](#librarygetpackage)
- [`library/publish`](#librarypublish) - [`library/publish`](#librarypublish)
- [`library/preinstall`](#librarypreinstall) - [`library/preinstall`](#librarypreinstall)
- [Errors](#errors-75) - [Errors](#errors-75)
@ -204,6 +212,7 @@ transport formats, please look [here](./protocol-architecture).
- [`LibraryNotResolved`](#librarynotresolved) - [`LibraryNotResolved`](#librarynotresolved)
- [`InvalidLibraryName`](#invalidlibraryname) - [`InvalidLibraryName`](#invalidlibraryname)
- [`DependencyDiscoveryError`](#dependencydiscoveryerror) - [`DependencyDiscoveryError`](#dependencydiscoveryerror)
- [`InvalidSemverVersion`](#invalidsemverversion)
<!-- /MarkdownTOC --> <!-- /MarkdownTOC -->
@ -1418,6 +1427,114 @@ interface NamedEdition {
} }
``` ```
### `ComponentGroups`
The description of component groups provided by the package. Object fields can
be omitted if the corresponding list is empty.
```typescript
interface ComponentGroups {
/** The list of component groups provided by the package. */
newGroups?: ComponentGroup[];
/** 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;
}
```
### `LibraryComponentGroup`
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`.
*/
library: string;
/** The module name without the library name prefix.
* E.g. given the `Standard.Base.Data.Vector` module reference,
* the `module` field contains `Data.Vector`.
*/
module: string;
color?: string;
icon?: string;
/** The list of components provided by this component group. */
exports: Component[];
}
```
## Connection Management ## Connection Management
In order to properly set-up and tear-down the language server connection, we In order to properly set-up and tear-down the language server connection, we
@ -4305,6 +4422,33 @@ To get local libraries that are not directly referenced in the edition, use
#### Errors #### Errors
- [`EditionNotFoundError`](#editionnotfounderror) indicates that the requested
edition, or an edition referenced in one of its parents, could not be found.
- [`FileSystemError`](#filesystemerror) to signal a generic, unrecoverable
file-system error.
### `editions/listDefinedComponents`
Lists all the component groups defined in an edition.
#### Parameters
```typescript
{
edition: EditionReference;
}
```
#### Result
```typescript
{
availableComponents: LibraryComponentGroup[];
}
```
#### Errors
- [`EditionNotFoundError`](#editionnotfounderror) indicates that the requested - [`EditionNotFoundError`](#editionnotfounderror) indicates that the requested
edition, or an edition referenced in one of its parents, could not be found. edition, or an edition referenced in one of its parents, could not be found.
- [`FileSystemError`](#filesystemerror) to signal a generic, unrecoverable - [`FileSystemError`](#filesystemerror) to signal a generic, unrecoverable
@ -4412,6 +4556,8 @@ All returned fields are optional, as they may be missing.
- [`LocalLibraryNotFound`](#locallibrarynotfound) to signal that a local library - [`LocalLibraryNotFound`](#locallibrarynotfound) to signal that a local library
with the given name does not exist on the local libraries path. with the given name does not exist on the local libraries path.
- [`InvalidSemverVersion`](#invalidsemverversion) to signal that the provided
version string is not a valid semver version.
- [`FileSystemError`](#filesystemerror) to signal a generic, unrecoverable - [`FileSystemError`](#filesystemerror) to signal a generic, unrecoverable
file-system error. file-system error.
@ -4446,6 +4592,47 @@ null;
- [`FileSystemError`](#filesystemerror) to signal a generic, unrecoverable - [`FileSystemError`](#filesystemerror) to signal a generic, unrecoverable
file-system error. file-system error.
### `library/getPackage`
Gets the package config associated with a specific library version.
If the version is `LocalLibraryVersion`, it will try to read the package file of
the local library and return an empty result if the manifest does not exist.
If the version is `PublishedLibraryVersion`, it will fetch the package config
from the library repository. A cached package config may also be used, if it is
available.
All returned fields are optional, as they may be missing.
#### Parameters
```typescript
{
namespace: String;
name: String;
version: LibraryVersion;
}
```
#### Results
```typescript
{
license?: String;
componentGroups?: ComponentGroups;
}
```
#### Errors
- [`LocalLibraryNotFound`](#locallibrarynotfound) to signal that a local library
with the given name does not exist on the local libraries path.
- [`InvalidSemverVersion`](#invalidsemverversion) to signal that the provided
version string is not a valid semver version.
- [`FileSystemError`](#filesystemerror) to signal a generic, unrecoverable
file-system error.
### `library/publish` ### `library/publish`
Publishes a library located in the local libraries directory to the main Enso Publishes a library located in the local libraries directory to the main Enso
@ -5083,3 +5270,18 @@ dependencies of the requested library.
"message" : "Error occurred while discovering dependencies: <reason>." "message" : "Error occurred while discovering dependencies: <reason>."
} }
``` ```
### `InvalidSemverVersion`
Signals that the provided version string is not a valid semver version. The
message contains the invalid version in the payload.
```typescript
"error" : {
"code" : 8011,
"message" : "[<invalid-version>] is not a valid semver version.",
"payload" : {
"version" : "<invalid-version>"
}
}
```

View File

@ -0,0 +1,196 @@
package org.enso.languageserver.libraries
import org.enso.editions.LibraryName
import org.enso.pkg.{
ComponentGroup,
ComponentGroups,
Config,
ExtendedComponentGroup,
ModuleReference
}
import scala.collection.immutable.ListMap
import scala.collection.{mutable, View}
/** The module allowing to resolve the dependencies between the component groups
* of different packages.
*/
final class ComponentGroupsResolver {
/** Run the component groups resolution algorithm.
*
* A package can define a new component group or extend an existing one. The
* resolving algorithm takes the component groups defined by the packages and
* applies the available extensions.
*
* @param packages the list of package configs
* @return the list of component groups with the dependencies resolved
*/
def run(packages: Iterable[Config]): Vector[LibraryComponentGroup] = {
val libraryComponents = packages
.map { config =>
LibraryName(config.namespace, config.name) -> config.componentGroups
}
.collect { case (libraryName, Right(componentGroups)) =>
libraryName -> componentGroups
}
val libraryComponentsMap =
ComponentGroupsResolver
.toMapKeepFirst(libraryComponents)
.to(ListMap)
resolveComponentGroups(libraryComponentsMap)
}
/** Resolve the component groups. Utility method that takes a list of
* component groups associated with the library name.
*
* @param libraryComponents the associated list of component groups
* @return the list of component groups with dependencies resolved
*/
private def resolveComponentGroups(
libraryComponents: Map[LibraryName, ComponentGroups]
): Vector[LibraryComponentGroup] = {
val newLibraryComponentGroups: View[LibraryComponentGroup] =
libraryComponents.view
.flatMap { case (libraryName, componentGroups) =>
componentGroups.newGroups.map(toLibraryComponentGroup(libraryName, _))
}
val newLibraryComponentGroupsMap
: Map[ModuleReference, LibraryComponentGroup] =
ComponentGroupsResolver
.groupByKeepFirst(newLibraryComponentGroups) { libraryComponentGroup =>
ModuleReference(
libraryComponentGroup.library,
libraryComponentGroup.module
)
}
val extendedComponentGroups: View[ExtendedComponentGroup] =
libraryComponents.view
.flatMap { case (_, componentGroups) =>
componentGroups.extendedGroups
}
val extendedComponentGroupsMap
: Map[ModuleReference, Vector[ExtendedComponentGroup]] =
ComponentGroupsResolver
.groupByKeepOrder(extendedComponentGroups)(_.module)
applyExtendedComponentGroups(
newLibraryComponentGroupsMap,
extendedComponentGroupsMap
)
}
/** Applies the extended component groups to the existing ones.
*
* @param libraryComponentGroups the list of component groups defined by
* packages
* @param extendedComponentGroups the list of component groups extending
* existing ones
* @return the list of component groups after extended component groups being
* applied
*/
private def applyExtendedComponentGroups(
libraryComponentGroups: Map[ModuleReference, LibraryComponentGroup],
extendedComponentGroups: Map[ModuleReference, Seq[ExtendedComponentGroup]]
): Vector[LibraryComponentGroup] =
libraryComponentGroups.map { case (module, libraryComponentGroup) =>
extendedComponentGroups
.get(module)
.fold(libraryComponentGroup) { extendedComponentGroups =>
extendedComponentGroups
.foldLeft(libraryComponentGroup)(applyExtendedComponentGroup)
}
}.toVector
/** Applies the extended component group to the target component group.
*
* @param libraryComponentGroup the target component group
* @param extendedComponentGroup the component group to apply
* @return the resulting component group
*/
private def applyExtendedComponentGroup(
libraryComponentGroup: LibraryComponentGroup,
extendedComponentGroup: ExtendedComponentGroup
): LibraryComponentGroup =
libraryComponentGroup.copy(
exports = libraryComponentGroup.exports :++ extendedComponentGroup.exports
)
/** 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 {
/** Partitions this collection into a map according to some discriminator
* function, dropping duplicated keys, and preserving the order of elements.
* E.g. if the discriminator function produces same keys for different
* values, the resulting map will contain only the first encountered
* key-value pair for that key.
*
* @param xs the source collection
* @param f the discriminator function
* @return the grouped collection preserving the order of elements
*/
private def groupByKeepFirst[K, V](xs: Iterable[V])(f: V => K): Map[K, V] =
xs
.foldLeft(mutable.LinkedHashMap.empty[K, V]) { (m, v) =>
val k = f(v)
if (m.contains(k)) m
else m += k -> v
}
.toMap
/** Partitions this collection into a map according to some discriminator
* function and preserving the order of elements.
*
* @param xs the source collection
* @param f the discriminator function
* @return the grouped collection that preserves the order of elements
*/
private def groupByKeepOrder[K, V](
xs: Iterable[V]
)(f: V => K): Map[K, Vector[V]] =
xs
.foldLeft(mutable.LinkedHashMap.empty[K, Vector[V]]) { (m, v) =>
m.updateWith(f(v)) {
case Some(xs) => Some(xs :+ v)
case None => Some(Vector(v))
}
m
}
.toMap
/** Convert the collection of key-value pairs into a map, dropping duplicated
* keys, and preserving the order of elements.
*
* @param xs the collection of key-value pairs
* @return the map preserving the order of elements
*/
private def toMapKeepFirst[K, V](xs: Iterable[(K, V)]): Map[K, V] =
xs
.foldLeft(mutable.LinkedHashMap.empty[K, V]) { case (m, (k, v)) =>
if (m.contains(k)) m
else m += k -> v
}
.toMap
}

View File

@ -0,0 +1,149 @@
package org.enso.languageserver.libraries
import org.enso.editions.LibraryName
import org.enso.pkg.{ComponentGroup, Config, ModuleReference}
import scala.collection.mutable
/** Validate the component groups of provided packages. */
final class ComponentGroupsValidator {
import ComponentGroupsValidator.ValidationError
/** Run the validation.
*
* The algorithm checks that the provided component groups are consistent:
* - Package configs have valid component groups structure
* - Packages don't define duplicate component groups
* - Packages override existing component groups
*
* @param packages the list of package configs
* @return the validation result for each package
*/
def validate(
packages: Iterable[Config]
): Iterable[Either[ValidationError, Config]] = {
val init: Iterable[Right[ValidationError, Config]] = packages.map(Right(_))
val modulesMap: mutable.Map[ModuleReference, ComponentGroup] = mutable.Map()
runValidation(init)(
validateInvalidComponentGroups,
validateDuplicateComponentGroups(modulesMap),
validateComponentGroupExtendsNothing(modulesMap)
)
}
private def validateInvalidComponentGroups
: Config => Either[ValidationError, Config] = { config =>
val libraryName = LibraryName(config.namespace, config.name)
config.componentGroups match {
case Right(_) =>
Right(config)
case Left(e) =>
Left(
ValidationError.InvalidComponentGroups(libraryName, e.getMessage())
)
}
}
private def validateDuplicateComponentGroups(
modulesMap: mutable.Map[ModuleReference, ComponentGroup]
): Config => Either[ValidationError, Config] = { config =>
val libraryName = LibraryName(config.namespace, config.name)
config.componentGroups.toOption
.flatMap { componentGroups =>
componentGroups.newGroups
.map { componentGroup =>
val moduleReference =
ModuleReference(libraryName, componentGroup.module)
if (modulesMap.contains(moduleReference)) {
Left(
ValidationError
.DuplicatedComponentGroup(libraryName, moduleReference)
)
} else {
modulesMap += moduleReference -> componentGroup
Right(config)
}
}
.find(_.isLeft)
}
.getOrElse(Right(config))
}
private def validateComponentGroupExtendsNothing(
modulesMap: mutable.Map[ModuleReference, ComponentGroup]
): Config => Either[ValidationError, Config] = { config =>
val libraryName = LibraryName(config.namespace, config.name)
config.componentGroups.toOption
.flatMap { componentGroups =>
componentGroups.extendedGroups
.map { extendedComponentGroup =>
if (modulesMap.contains(extendedComponentGroup.module)) {
Right(config)
} else {
Left(
ValidationError.ComponentGroupExtendsNothing(
libraryName,
extendedComponentGroup.module
)
)
}
}
.find(_.isLeft)
}
.getOrElse(Right(config))
}
private def runValidation[A, E](xs: Iterable[Either[E, A]])(
fs: A => Either[E, A]*
): Iterable[Either[E, A]] =
fs.foldLeft(xs) { (xs, f) =>
xs.map {
case Right(a) => f(a)
case Left(e) => Left(e)
}
}
}
object ComponentGroupsValidator {
/** Base trait for validation results. */
sealed trait ValidationError
object ValidationError {
/** An error indicating that the package config defines duplicate component
* group.
*
* @param libraryName the library defining duplicate component group
* @param moduleReference the duplicated module reference
*/
case class DuplicatedComponentGroup(
libraryName: LibraryName,
moduleReference: ModuleReference
) extends ValidationError
/** An error indicating that the package config has invalid component groups
* format.
*
* @param libraryName the library name defining invalid component groups
* @param message the error message
*/
case class InvalidComponentGroups(
libraryName: LibraryName,
message: String
) extends ValidationError
/** An error indicating that the library defines a component group extension
* that extends non-existent component group.
*
* @param libraryName the library defining problematic component group
* extension
* @param moduleReference the module reference to non-existent module
*/
case class ComponentGroupExtendsNothing(
libraryName: LibraryName,
moduleReference: ModuleReference
) extends ValidationError
}
}

View File

@ -1,10 +1,10 @@
package org.enso.languageserver.libraries package org.enso.languageserver.libraries
import io.circe.Json import io.circe.{Json, JsonObject}
import io.circe.literal.JsonStringContext import io.circe.literal.JsonStringContext
import org.enso.editions.{LibraryName, LibraryVersion} import org.enso.editions.{LibraryName, LibraryVersion}
import org.enso.jsonrpc.{Error, HasParams, HasResult, Method, Unused} import org.enso.jsonrpc.{Error, HasParams, HasResult, Method, Unused}
import org.enso.pkg.Contact import org.enso.pkg.{ComponentGroups, Contact}
object LibraryApi { object LibraryApi {
case object EditionsListAvailable extends Method("editions/listAvailable") { case object EditionsListAvailable extends Method("editions/listAvailable") {
@ -98,6 +98,21 @@ object LibraryApi {
} }
} }
case object EditionsListDefinedComponents
extends Method("editions/listDefinedComponents") { self =>
case class Params(edition: EditionReference)
case class Result(availableComponents: Seq[LibraryComponentGroup])
implicit val hasParams = new HasParams[this.type] {
type Params = self.Params
}
implicit val hasResult = new HasResult[this.type] {
type Result = self.Result
}
}
case object LibraryListLocal extends Method("library/listLocal") { self => case object LibraryListLocal extends Method("library/listLocal") { self =>
case class Result(localLibraries: Seq[LibraryEntry]) case class Result(localLibraries: Seq[LibraryEntry])
@ -163,6 +178,30 @@ object LibraryApi {
} }
} }
case object LibraryGetPackage extends Method("library/getPackage") { self =>
case class Params(
namespace: String,
name: String,
version: LibraryEntry.LibraryVersion
)
// TODO[DB]: raw package was added to response as a temporary field and
// should be removed when the integration with IDE is finished
case class Result(
license: Option[String],
componentGroups: Option[ComponentGroups],
raw: JsonObject
)
implicit val hasParams = new HasParams[this.type] {
type Params = self.Params
}
implicit val hasResult = new HasResult[this.type] {
type Result = self.Result
}
}
case object LibraryPublish extends Method("library/publish") { self => case object LibraryPublish extends Method("library/publish") { self =>
case class Params( case class Params(
@ -256,4 +295,12 @@ object LibraryApi {
8010, 8010,
s"Error occurred while discovering dependencies: $reason." s"Error occurred while discovering dependencies: $reason."
) )
case class InvalidSemverVersion(version: String)
extends Error(8011, s"[$version] is not a valid semver version.") {
override def payload: Option[Json] = Some(
json"""{ "version" : $version }"""
)
}
} }

View File

@ -0,0 +1,59 @@
package org.enso.languageserver.libraries
import io.circe._
import io.circe.syntax._
import org.enso.editions.LibraryName
import org.enso.pkg.{Component, ModuleName}
/** The component group definition of a library.
*
* @param library the library name
* @param module the module name
* @param color the component group color
* @param icon the component group icon
* @param exports the list of components provided by this component group
*/
case class LibraryComponentGroup(
library: LibraryName,
module: ModuleName,
color: Option[String],
icon: Option[String],
exports: Seq[Component]
)
object LibraryComponentGroup {
/** Fields for use when serializing the [[LibraryComponentGroup]]. */
private object Fields {
val Library = "library"
val Module = "module"
val Color = "color"
val Icon = "icon"
val Exports = "exports"
}
/** [[Encoder]] instance for the [[LibraryComponentGroup]]. */
implicit val encoder: Encoder[LibraryComponentGroup] = { componentGroup =>
val color = componentGroup.color.map(Fields.Color -> _.asJson)
val icon = componentGroup.icon.map(Fields.Icon -> _.asJson)
val exports = Option.unless(componentGroup.exports.isEmpty)(
Fields.Exports -> componentGroup.exports.asJson
)
Json.obj(
(Fields.Library -> componentGroup.library.asJson) +:
(Fields.Module -> componentGroup.module.asJson) +:
(color.toSeq ++ icon.toSeq ++ exports.toSeq): _*
)
}
/** [[Decoder]] instance for the [[LibraryComponentGroup]]. */
implicit val decoder: Decoder[LibraryComponentGroup] = { json =>
for {
library <- json.get[LibraryName](Fields.Library)
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())
} yield LibraryComponentGroup(library, module, color, icon, exports)
}
}

View File

@ -2,7 +2,6 @@ package org.enso.languageserver.libraries
import akka.actor.Props import akka.actor.Props
import com.typesafe.scalalogging.LazyLogging import com.typesafe.scalalogging.LazyLogging
import org.enso.distribution.FileSystem.PathSyntax
import org.enso.distribution.{DistributionManager, FileSystem} import org.enso.distribution.{DistributionManager, FileSystem}
import org.enso.editions.{Editions, LibraryName} import org.enso.editions.{Editions, LibraryName}
import org.enso.languageserver.libraries.LocalLibraryManagerProtocol._ import org.enso.languageserver.libraries.LocalLibraryManagerProtocol._
@ -12,11 +11,12 @@ import org.enso.librarymanager.local.{
} }
import org.enso.librarymanager.published.repository.LibraryManifest import org.enso.librarymanager.published.repository.LibraryManifest
import org.enso.pkg.validation.NameValidation import org.enso.pkg.validation.NameValidation
import org.enso.pkg.{Contact, PackageManager} import org.enso.pkg.{Config, Contact, Package, PackageManager}
import org.enso.yaml.YamlHelper import org.enso.yaml.YamlHelper
import java.io.File import java.io.File
import java.nio.file.{Files, Path} import java.nio.file.{Files, Path}
import scala.util.{Success, Try} import scala.util.{Success, Try}
/** An Actor that manages local libraries. */ /** An Actor that manages local libraries. */
@ -41,6 +41,8 @@ class LocalLibraryManager(
tagLine = request.tagLine tagLine = request.tagLine
) )
) )
case GetPackage(libraryName) =>
startRequest(getPackage(libraryName))
case ListLocalLibraries => case ListLocalLibraries =>
startRequest(listLocalLibraries()) startRequest(listLocalLibraries())
case Create(libraryName, authors, maintainers, license) => case Create(libraryName, authors, maintainers, license) =>
@ -190,6 +192,28 @@ class LocalLibraryManager(
_ = saveManifest(manifestPath, updatedManifest) _ = saveManifest(manifestPath, updatedManifest)
} yield EmptyResponse() } yield EmptyResponse()
/** Loads the package config for a local library. */
private def getPackage(libraryName: LibraryName): Try[GetPackageResponse] =
for {
libraryRootPath <- localLibraryProvider
.findLibrary(libraryName)
.toRight(LocalLibraryNotFoundError(libraryName))
.toTry
configPath = libraryRootPath / Package.configFileName
config <- loadPackageConfig(configPath)
} yield {
if (config.componentGroups.isLeft) {
logger.error(
s"Failed to parse library [$libraryName] component groups."
)
}
GetPackageResponse(
license = config.license,
componentGroups = config.componentGroups.toOption,
rawPackage = config.originalJson
)
}
/** Tries to load the manifest. /** Tries to load the manifest.
* *
* If the file does not exist, an empty manifest is returned. Any other * If the file does not exist, an empty manifest is returned. Any other
@ -206,6 +230,10 @@ class LocalLibraryManager(
manifest: LibraryManifest manifest: LibraryManifest
): Unit = FileSystem.writeTextFile(manifestPath, YamlHelper.toYaml(manifest)) ): Unit = FileSystem.writeTextFile(manifestPath, YamlHelper.toYaml(manifest))
/** Load the package config. */
private def loadPackageConfig(packagePath: Path): Try[Config] =
YamlHelper.load[Config](packagePath)
/** Finds the edition associated with the current project, if specified in its /** Finds the edition associated with the current project, if specified in its
* config. * config.
*/ */

View File

@ -0,0 +1,21 @@
package org.enso.languageserver.libraries
import org.enso.jsonrpc
/** The object mapping the [[LocalLibraryManagerProtocol]] failures into the
* corresponding JSONRPC error messages.
*/
object LocalLibraryManagerFailureMapper {
/** Convert the [[LocalLibraryManagerProtocol.Failure]] into the corresponding
* JSONRPC error message.
*
* @param error the failure object
* @return the JSONRPC error message
*/
def mapFailure(error: LocalLibraryManagerProtocol.Failure): jsonrpc.Error =
error match {
case LocalLibraryManagerProtocol.InvalidSemverVersionError(version) =>
LibraryApi.InvalidSemverVersion(version)
}
}

View File

@ -1,9 +1,9 @@
package org.enso.languageserver.libraries package org.enso.languageserver.libraries
import io.circe.JsonObject
import org.enso.editions.LibraryName import org.enso.editions.LibraryName
import org.enso.pkg.Contact import org.enso.librarymanager.resolved.LibraryRoot
import org.enso.pkg.{ComponentGroups, Contact}
import java.nio.file.Path
object LocalLibraryManagerProtocol { object LocalLibraryManagerProtocol {
@ -26,6 +26,16 @@ object LocalLibraryManagerProtocol {
tagLine: Option[String] tagLine: Option[String]
) extends Request ) extends Request
/** A request to get the library package. */
case class GetPackage(libraryName: LibraryName) extends Request
/** A response to the [[GetPackage]] request. */
case class GetPackageResponse(
license: String,
componentGroups: Option[ComponentGroups],
rawPackage: JsonObject
)
/** A request to list local libraries. */ /** A request to list local libraries. */
case object ListLocalLibraries extends Request case object ListLocalLibraries extends Request
@ -44,7 +54,7 @@ object LocalLibraryManagerProtocol {
case class FindLibrary(libraryName: LibraryName) extends Request case class FindLibrary(libraryName: LibraryName) extends Request
/** A response to [[FindLibrary]]. */ /** A response to [[FindLibrary]]. */
case class FindLibraryResponse(libraryRoot: Option[Path]) case class FindLibraryResponse(libraryRoot: Option[LibraryRoot])
/** Indicates that a library with the given name was not found among local /** Indicates that a library with the given name was not found among local
* libraries. * libraries.
@ -59,4 +69,14 @@ object LocalLibraryManagerProtocol {
* Sent as a reply to [[Create]] and [[SetMetadata]]. * Sent as a reply to [[Create]] and [[SetMetadata]].
*/ */
case class EmptyResponse() case class EmptyResponse()
/** A base trait for failures. */
sealed trait Failure
/** An error indicating that the provided version is not a valid semver
* version.
*
* @version invalid version string
*/
case class InvalidSemverVersionError(version: String) extends Failure
} }

View File

@ -0,0 +1,165 @@
package org.enso.languageserver.libraries.handler
import akka.actor.{Actor, ActorRef, Props, Status}
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
}
import org.enso.languageserver.util.UnhandledLogging
import org.enso.librarymanager.local.LocalLibraryProvider
import org.enso.librarymanager.published.PublishedLibraryCache
import org.enso.pkg.Config
/** A request handler for the `editions/listDefinedComponents` endpoint.
*
* @param editionReferenceResolver an [[EditionReferenceResolver]] instance
* @param localLibraryProvider a provider of local libraries
* @param publishedLibraryCache a cache of published libraries
* @param componentGroupsResolver a module resolving the dependencies between
* component groups
*/
class EditionsListDefinedComponentsHandler(
editionReferenceResolver: EditionReferenceResolver,
localLibraryProvider: LocalLibraryProvider,
publishedLibraryCache: PublishedLibraryCache,
componentGroupsResolver: ComponentGroupsResolver,
componentGroupsValidator: ComponentGroupsValidator
) extends Actor
with LazyLogging
with UnhandledLogging {
import context.dispatcher
override def receive: Receive = requestStage
private def requestStage: Receive = {
case Request(
EditionsListDefinedComponents,
id,
EditionsListDefinedComponents.Params(reference)
) =>
BlockingOperation
.run {
val edition = editionReferenceResolver.resolveEdition(reference).get
val definedLibraries = edition.getAllDefinedLibraries.view
.map { case (name, version) =>
readLocalPackage(name, version)
}
.collect { case Some(config) =>
config
}
val validationResults = componentGroupsValidator
.validate(definedLibraries)
validationResults
.collect { case Left(error) => error }
.foreach(logValidationError)
val validatedLibraries = validationResults
.collect { case Right(config) => config }
componentGroupsResolver.run(validatedLibraries)
}
.map(EditionsListDefinedComponentsHandler.Result) pipeTo self
context.become(responseStage(id, sender()))
}
private def logValidationError(
error: ComponentGroupsValidator.ValidationError
): Unit =
error match {
case ComponentGroupsValidator.ValidationError
.InvalidComponentGroups(libraryName, message) =>
logger.warn(
s"Validation error. Failed to read library [$libraryName] " +
s"component groups (reason: $message)."
)
case ComponentGroupsValidator.ValidationError
.DuplicatedComponentGroup(libraryName, moduleReference) =>
logger.warn(
s"Validation error. Library [$libraryName] defines duplicate " +
s"component group [$moduleReference]."
)
case ComponentGroupsValidator.ValidationError
.ComponentGroupExtendsNothing(libraryName, moduleReference) =>
logger.warn(
s"Validation error. Library [$libraryName] component group " +
s"[$moduleReference] extends nothing."
)
}
private def responseStage(id: Id, replyTo: ActorRef): Receive = {
case EditionsListDefinedComponentsHandler.Result(components) =>
replyTo ! ResponseResult(
EditionsListDefinedComponents,
id,
EditionsListDefinedComponents.Result(components)
)
context.stop(self)
case Status.Failure(exception) =>
replyTo ! ResponseError(
Some(id),
FileSystemError(exception.getMessage)
)
context.stop(self)
}
private def readLocalPackage(
libraryName: LibraryName,
libraryVersion: LibraryVersion
): Option[Config] = {
val libraryPathOpt = libraryVersion match {
case LibraryVersion.Local =>
localLibraryProvider.findLibrary(libraryName)
case LibraryVersion.Published(version, _) =>
publishedLibraryCache.findCachedLibrary(libraryName, version)
}
libraryPathOpt.flatMap { libraryPath =>
libraryPath.getReadAccess.readPackage().toOption
}
}
}
object EditionsListDefinedComponentsHandler {
private case class Result(components: Seq[LibraryComponentGroup])
/** Creates a configuration object to create
* [[EditionsListDefinedComponentsHandler]].
*
* @param editionReferenceResolver an [[EditionReferenceResolver]] instance
* @param localLibraryProvider a provider of local libraries
* @param publishedLibraryCache a cache of published libraries
* @param componentGroupsResolver a module resolving the dependencies
* between component groups
* @param componentGroupsValidator a module that checks component groups for
* consistency
*/
def props(
editionReferenceResolver: EditionReferenceResolver,
localLibraryProvider: LocalLibraryProvider,
publishedLibraryCache: PublishedLibraryCache,
componentGroupsResolver: ComponentGroupsResolver =
new ComponentGroupsResolver,
componentGroupsValidator: ComponentGroupsValidator =
new ComponentGroupsValidator
): Props = Props(
new EditionsListDefinedComponentsHandler(
editionReferenceResolver,
localLibraryProvider,
publishedLibraryCache,
componentGroupsResolver,
componentGroupsValidator
)
)
}

View File

@ -1,6 +1,8 @@
package org.enso.languageserver.libraries.handler package org.enso.languageserver.libraries.handler
import akka.actor.{Actor, ActorRef, Cancellable, Props, Status} import akka.actor.{Actor, ActorRef, Cancellable, Props, Status}
import scala.util.{Success, Try}
import akka.pattern.pipe import akka.pattern.pipe
import com.typesafe.scalalogging.LazyLogging import com.typesafe.scalalogging.LazyLogging
import nl.gn0s1s.bump.SemVer import nl.gn0s1s.bump.SemVer
@ -11,23 +13,27 @@ import org.enso.languageserver.filemanager.FileManagerApi.FileSystemError
import org.enso.languageserver.libraries.LibraryApi._ import org.enso.languageserver.libraries.LibraryApi._
import org.enso.languageserver.libraries.{ import org.enso.languageserver.libraries.{
LibraryEntry, LibraryEntry,
LocalLibraryManagerFailureMapper,
LocalLibraryManagerProtocol LocalLibraryManagerProtocol
} }
import org.enso.languageserver.requesthandler.RequestTimeout import org.enso.languageserver.requesthandler.RequestTimeout
import org.enso.languageserver.util.UnhandledLogging import org.enso.languageserver.util.UnhandledLogging
import org.enso.librarymanager.published.PublishedLibraryCache
import org.enso.librarymanager.published.repository.RepositoryHelper.RepositoryMethods import org.enso.librarymanager.published.repository.RepositoryHelper.RepositoryMethods
import scala.concurrent.Future import scala.concurrent.Future
import scala.concurrent.duration.FiniteDuration import scala.concurrent.duration.FiniteDuration
/** A request handler for the `library/create` endpoint. /** A request handler for the `library/getMetadata` endpoint.
* *
* @param timeout request timeout * @param timeout request timeout
* @param localLibraryManager reference to the local library manager actor * @param localLibraryManager reference to the local library manager actor
* @param publishedLibraryCache the cache of published libraries
*/ */
class LibraryGetMetadataHandler( class LibraryGetMetadataHandler(
timeout: FiniteDuration, timeout: FiniteDuration,
localLibraryManager: ActorRef localLibraryManager: ActorRef,
publishedLibraryCache: PublishedLibraryCache
) extends Actor ) extends Actor
with LazyLogging with LazyLogging
with UnhandledLogging { with UnhandledLogging {
@ -48,11 +54,18 @@ class LibraryGetMetadataHandler(
libraryName libraryName
) )
case LibraryEntry.PublishedLibraryVersion(version, repositoryUrl) => case LibraryEntry.PublishedLibraryVersion(version, repositoryUrl) =>
fetchPublishedMetadata( SemVer(version) match {
case Some(semVerVersion) =>
getOrFetchPublishedMetadata(
libraryName, libraryName,
version, semVerVersion,
repositoryUrl repositoryUrl
) pipeTo self ) pipeTo self
case None =>
self ! LocalLibraryManagerProtocol.InvalidSemverVersionError(
version
)
}
} }
val cancellable = val cancellable =
@ -82,33 +95,59 @@ class LibraryGetMetadataHandler(
cancellable.cancel() cancellable.cancel()
context.stop(self) context.stop(self)
case failure: LocalLibraryManagerProtocol.Failure =>
replyTo ! LocalLibraryManagerFailureMapper.mapFailure(failure)
cancellable.cancel()
context.stop(self)
case Status.Failure(exception) => case Status.Failure(exception) =>
replyTo ! ResponseError(Some(id), FileSystemError(exception.getMessage)) replyTo ! ResponseError(Some(id), FileSystemError(exception.getMessage))
cancellable.cancel() cancellable.cancel()
context.stop(self) context.stop(self)
} }
// TODO [RW] Once the manifests of downloaded libraries are being cached, private def getOrFetchPublishedMetadata(
// it may be worth to try resolving the local cache first to avoid libraryName: LibraryName,
// downloading the manifest again. This should be done before the issues version: SemVer,
// #1772 or #1775 are completed. repositoryUrl: String
): Future[LocalLibraryManagerProtocol.GetMetadataResponse] =
getCachedMetadata(libraryName, version) match {
case Some(response) =>
response.fold(Future.failed, Future.successful)
case None =>
fetchPublishedMetadata(libraryName, version, repositoryUrl)
}
private def getCachedMetadata(
libraryName: LibraryName,
version: SemVer
): Option[Try[LocalLibraryManagerProtocol.GetMetadataResponse]] =
publishedLibraryCache
.findCachedLibrary(libraryName, version)
.map { libraryPath =>
libraryPath.getReadAccess
.readManifest()
.map { manifestAttempt =>
manifestAttempt.map(manifest =>
LocalLibraryManagerProtocol.GetMetadataResponse(
manifest.description,
manifest.tagLine
)
)
}
.getOrElse(
Success(LocalLibraryManagerProtocol.GetMetadataResponse(None, None))
)
}
private def fetchPublishedMetadata( private def fetchPublishedMetadata(
libraryName: LibraryName, libraryName: LibraryName,
version: String, version: SemVer,
repositoryUrl: String repositoryUrl: String
): Future[LocalLibraryManagerProtocol.GetMetadataResponse] = for { ): Future[LocalLibraryManagerProtocol.GetMetadataResponse] = for {
semver <- Future.fromTry(
SemVer(version)
.toRight(
new IllegalStateException(
s"Library version [$version] is not a valid semver string."
)
)
.toTry
)
manifest <- Repository(repositoryUrl) manifest <- Repository(repositoryUrl)
.accessLibrary(libraryName, semver) .accessLibrary(libraryName, version)
.downloadManifest() .fetchManifest()
.toFuture .toFuture
} yield LocalLibraryManagerProtocol.GetMetadataResponse( } yield LocalLibraryManagerProtocol.GetMetadataResponse(
description = manifest.description, description = manifest.description,
@ -122,7 +161,18 @@ object LibraryGetMetadataHandler {
* *
* @param timeout request timeout * @param timeout request timeout
* @param localLibraryManager reference to the local library manager actor * @param localLibraryManager reference to the local library manager actor
* @param publishedLibraryCache the cache of published libraries
*/ */
def props(timeout: FiniteDuration, localLibraryManager: ActorRef): Props = def props(
Props(new LibraryGetMetadataHandler(timeout, localLibraryManager)) timeout: FiniteDuration,
localLibraryManager: ActorRef,
publishedLibraryCache: PublishedLibraryCache
): Props =
Props(
new LibraryGetMetadataHandler(
timeout,
localLibraryManager,
publishedLibraryCache
)
)
} }

View File

@ -0,0 +1,179 @@
package org.enso.languageserver.libraries.handler
import akka.actor.{Actor, ActorRef, Cancellable, Props, Status}
import akka.pattern.pipe
import com.typesafe.scalalogging.LazyLogging
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.requesthandler.RequestTimeout
import org.enso.languageserver.util.UnhandledLogging
import org.enso.librarymanager.published.PublishedLibraryCache
import org.enso.librarymanager.published.repository.RepositoryHelper.RepositoryMethods
import scala.concurrent.Future
import scala.concurrent.duration.FiniteDuration
import scala.util.Try
/** A request handler for the `library/getPackage` endpoint.
*
* @param timeout request timeout
* @param localLibraryManager reference to the local library manager actor
* @param publishedLibraryCache the cache of published libraries
*/
class LibraryGetPackageHandler(
timeout: FiniteDuration,
localLibraryManager: ActorRef,
publishedLibraryCache: PublishedLibraryCache
) extends Actor
with LazyLogging
with UnhandledLogging {
import context.dispatcher
override def receive: Receive = requestStage
private def requestStage: Receive = {
case Request(
LibraryGetPackage,
id,
LibraryGetPackage.Params(namespace, name, version)
) =>
val libraryName = LibraryName(namespace, name)
version match {
case LibraryEntry.LocalLibraryVersion =>
localLibraryManager ! LocalLibraryManagerProtocol.GetPackage(
libraryName
)
case LibraryEntry.PublishedLibraryVersion(version, repositoryUrl) =>
SemVer(version) match {
case Some(semVerVersion) =>
getOrFetchPublishedPackage(
libraryName,
semVerVersion,
repositoryUrl
) pipeTo self
case None =>
self ! LocalLibraryManagerProtocol.InvalidSemverVersionError(
version
)
}
}
val cancellable =
context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout)
context.become(responseStage(id, sender(), cancellable))
}
private def responseStage(
id: Id,
replyTo: ActorRef,
cancellable: Cancellable
): Receive = {
case RequestTimeout =>
logger.error("Request [{}] timed out.", id)
replyTo ! ResponseError(Some(id), Errors.RequestTimeout)
context.stop(self)
case LocalLibraryManagerProtocol.GetPackageResponse(
license,
componentGroups,
rawPackage
) =>
replyTo ! ResponseResult(
LibraryGetPackage,
id,
LibraryGetPackage.Result(
Option.unless(license.isEmpty)(license),
componentGroups,
rawPackage
)
)
cancellable.cancel()
context.stop(self)
case failure: LocalLibraryManagerProtocol.Failure =>
replyTo ! LocalLibraryManagerFailureMapper.mapFailure(failure)
cancellable.cancel()
context.stop(self)
case Status.Failure(exception) =>
replyTo ! ResponseError(Some(id), FileSystemError(exception.getMessage))
cancellable.cancel()
context.stop(self)
}
private def getOrFetchPublishedPackage(
libraryName: LibraryName,
version: SemVer,
repositoryUrl: String
): Future[LocalLibraryManagerProtocol.GetPackageResponse] =
getCachedPackage(libraryName, version) match {
case Some(response) =>
response.fold(Future.failed, Future.successful)
case None =>
fetchPublishedPackage(libraryName, version, repositoryUrl)
}
private def getCachedPackage(
libraryName: LibraryName,
version: SemVer
): Option[Try[LocalLibraryManagerProtocol.GetPackageResponse]] =
publishedLibraryCache
.findCachedLibrary(libraryName, version)
.map { libraryPath =>
libraryPath.getReadAccess
.readPackage()
.map(config =>
LocalLibraryManagerProtocol.GetPackageResponse(
config.license,
config.componentGroups.toOption,
config.originalJson
)
)
}
private def fetchPublishedPackage(
libraryName: LibraryName,
version: SemVer,
repositoryUrl: String
): Future[LocalLibraryManagerProtocol.GetPackageResponse] = for {
config <- Repository(repositoryUrl)
.accessLibrary(libraryName, version)
.fetchPackageConfig()
.toFuture
} yield LocalLibraryManagerProtocol.GetPackageResponse(
license = config.license,
componentGroups = config.componentGroups.toOption,
rawPackage = config.originalJson
)
}
object LibraryGetPackageHandler {
/** Creates a configuration object to create [[LibraryGetPackageHandler]].
*
* @param timeout request timeout
* @param localLibraryManager reference to the local library manager actor
* @param publishedLibraryCache the cache of published libraries
*/
def props(
timeout: FiniteDuration,
localLibraryManager: ActorRef,
publishedLibraryCache: PublishedLibraryCache
): Props =
Props(
new LibraryGetPackageHandler(
timeout,
localLibraryManager,
publishedLibraryCache
)
)
}

View File

@ -107,7 +107,7 @@ class LibraryPublishHandler(
new CompilerBasedDependencyExtractor(logLevel) new CompilerBasedDependencyExtractor(logLevel)
LibraryUploader(dependencyExtractor) LibraryUploader(dependencyExtractor)
.uploadLibrary( .uploadLibrary(
libraryRoot, libraryRoot.location,
uploadUrl, uploadUrl,
token, token,
progressReporter progressReporter

View File

@ -73,6 +73,7 @@ import org.enso.polyglot.runtime.Runtime.Api
import org.enso.polyglot.runtime.Runtime.Api.ProgressNotification import org.enso.polyglot.runtime.Runtime.Api.ProgressNotification
import java.util.UUID import java.util.UUID
import scala.concurrent.duration._ import scala.concurrent.duration._
/** An actor handling communications between a single client and the language /** An actor handling communications between a single client and the language
@ -506,18 +507,33 @@ class JsonConnectionController(
.props(requestTimeout, projectSettingsManager), .props(requestTimeout, projectSettingsManager),
EditionsSetLocalLibrariesPreference -> EditionsSetProjectLocalLibrariesPreferenceHandler EditionsSetLocalLibrariesPreference -> EditionsSetProjectLocalLibrariesPreferenceHandler
.props(requestTimeout, projectSettingsManager), .props(requestTimeout, projectSettingsManager),
EditionsListDefinedComponents -> EditionsListDefinedComponentsHandler
.props(
libraryConfig.editionReferenceResolver,
libraryConfig.localLibraryProvider,
libraryConfig.publishedLibraryCache
),
LibraryCreate -> LibraryCreateHandler LibraryCreate -> LibraryCreateHandler
.props(requestTimeout, libraryConfig.localLibraryManager), .props(requestTimeout, libraryConfig.localLibraryManager),
LibraryListLocal -> LibraryListLocalHandler LibraryListLocal -> LibraryListLocalHandler
.props(requestTimeout, libraryConfig.localLibraryManager), .props(requestTimeout, libraryConfig.localLibraryManager),
LibraryGetMetadata -> LibraryGetMetadataHandler LibraryGetMetadata -> LibraryGetMetadataHandler
.props(requestTimeout, libraryConfig.localLibraryManager), .props(
requestTimeout,
libraryConfig.localLibraryManager,
libraryConfig.publishedLibraryCache
),
LibraryPreinstall -> LibraryPreinstallHandler LibraryPreinstall -> LibraryPreinstallHandler
.props(libraryConfig.editionReferenceResolver, libraryConfig), .props(libraryConfig.editionReferenceResolver, libraryConfig),
LibraryPublish -> LibraryPublishHandler LibraryPublish -> LibraryPublishHandler
.props(requestTimeout, libraryConfig.localLibraryManager), .props(requestTimeout, libraryConfig.localLibraryManager),
LibrarySetMetadata -> LibrarySetMetadataHandler LibrarySetMetadata -> LibrarySetMetadataHandler
.props(requestTimeout, libraryConfig.localLibraryManager) .props(requestTimeout, libraryConfig.localLibraryManager),
LibraryGetPackage -> LibraryGetPackageHandler.props(
requestTimeout,
libraryConfig.localLibraryManager,
libraryConfig.publishedLibraryCache
)
) )
} }

View File

@ -76,10 +76,12 @@ object JsonRpc {
.registerRequest(EditionsSetParentEdition) .registerRequest(EditionsSetParentEdition)
.registerRequest(EditionsSetLocalLibrariesPreference) .registerRequest(EditionsSetLocalLibrariesPreference)
.registerRequest(EditionsListDefinedLibraries) .registerRequest(EditionsListDefinedLibraries)
.registerRequest(EditionsListDefinedComponents)
.registerRequest(LibraryListLocal) .registerRequest(LibraryListLocal)
.registerRequest(LibraryCreate) .registerRequest(LibraryCreate)
.registerRequest(LibraryGetMetadata) .registerRequest(LibraryGetMetadata)
.registerRequest(LibrarySetMetadata) .registerRequest(LibrarySetMetadata)
.registerRequest(LibraryGetPackage)
.registerRequest(LibraryPublish) .registerRequest(LibraryPublish)
.registerRequest(LibraryPreinstall) .registerRequest(LibraryPreinstall)
.registerNotification(TaskStarted) .registerNotification(TaskStarted)

View File

@ -0,0 +1,316 @@
package org.enso.languageserver.libraries
import org.enso.editions.LibraryName
import org.enso.pkg.{
Component,
ComponentGroup,
ComponentGroups,
Config,
ExtendedComponentGroup,
ModuleName,
ModuleReference
}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
class ComponentGroupsResolverSpec extends AnyWordSpec with Matchers {
import ComponentGroupsResolverSpec._
"ComponentGroupsResolver" should {
"return a list of defined component groups preserving the order" in {
val resolver = new ComponentGroupsResolver
val testPackages = Vector(
config("Foo", "Bar"),
config(
"Foo",
"Baz",
ComponentGroups(List(newComponentGroup("Mod1", "one", "two")), List())
),
config(
"Foo",
"Quux",
ComponentGroups(List(newComponentGroup("Mod2", "one")), List())
)
)
resolver.run(testPackages) shouldEqual Vector(
libraryComponentGroup("Foo", "Baz", "Mod1", "one", "two"),
libraryComponentGroup("Foo", "Quux", "Mod2", "one")
)
}
"drop duplicated library definitions" in {
val resolver = new ComponentGroupsResolver
val testPackages = Vector(
config("Foo", "Bar"),
config(
"Foo",
"Baz",
ComponentGroups(
List(newComponentGroup("Mod1", "one", "two")),
List()
)
),
config(
"Foo",
"Baz",
ComponentGroups(List(newComponentGroup("Mod1", "one")), List())
),
config(
"Foo",
"Quux",
ComponentGroups(List(newComponentGroup("Mod2", "one")), List())
)
)
resolver.run(testPackages) shouldEqual Vector(
libraryComponentGroup("Foo", "Baz", "Mod1", "one", "two"),
libraryComponentGroup("Foo", "Quux", "Mod2", "one")
)
}
"drop duplicated component group definitions" in {
val resolver = new ComponentGroupsResolver
val testPackages = Vector(
config("Foo", "Bar"),
config(
"Foo",
"Baz",
ComponentGroups(
List(
newComponentGroup("Mod1", "one", "two"),
newComponentGroup("Mod1", "three")
),
List()
)
),
config(
"Foo",
"Quux",
ComponentGroups(List(newComponentGroup("Mod2", "one")), List())
)
)
resolver.run(testPackages) shouldEqual Vector(
libraryComponentGroup("Foo", "Baz", "Mod1", "one", "two"),
libraryComponentGroup("Foo", "Quux", "Mod2", "one")
)
}
"apply extended component groups" in {
val resolver = new ComponentGroupsResolver
val testPackages = Vector(
config(
"user",
"Unnamed",
ComponentGroups(
List(newComponentGroup("Main", "main")),
List(
extendedComponentGroup("Standard", "Base", "Data.Vector", "quux")
)
)
),
config(
"Standard",
"Base",
ComponentGroups(
List(newComponentGroup("Data.Vector", "one", "two")),
List()
)
),
config(
"user",
"Vector_Utils",
ComponentGroups(
List(),
List(
extendedComponentGroup("Standard", "Base", "Data.Vector", "three")
)
)
)
)
resolver.run(testPackages) shouldEqual Vector(
libraryComponentGroup("user", "Unnamed", "Main", "main"),
libraryComponentGroup(
"Standard",
"Base",
"Data.Vector",
"one",
"two",
"quux",
"three"
)
)
}
"apply mutually extended component groups" in {
val resolver = new ComponentGroupsResolver
val testPackages = Vector(
config(
"Standard",
"Table",
ComponentGroups(
List(newComponentGroup("Data.Table", "first")),
List(
extendedComponentGroup("Standard", "Base", "Data.Vector", "quux")
)
)
),
config(
"Standard",
"Base",
ComponentGroups(
List(newComponentGroup("Data.Vector", "one", "two")),
List(
extendedComponentGroup(
"Standard",
"Table",
"Data.Table",
"second"
)
)
)
),
config(
"user",
"Vector_Utils",
ComponentGroups(
List(),
List(
extendedComponentGroup("Standard", "Base", "Data.Vector", "three")
)
)
)
)
resolver.run(testPackages) shouldEqual Vector(
libraryComponentGroup(
"Standard",
"Table",
"Data.Table",
"first",
"second"
),
libraryComponentGroup(
"Standard",
"Base",
"Data.Vector",
"one",
"two",
"quux",
"three"
)
)
}
"skip component groups extending nothing" in {
val resolver = new ComponentGroupsResolver
val testPackages = Vector(
config(
"user",
"Unnamed",
ComponentGroups(List(newComponentGroup("Main", "main")), List())
),
config(
"Standard",
"Base",
ComponentGroups(
List(newComponentGroup("Data.Vector", "one", "two")),
List()
)
),
config(
"user",
"Vector_Utils",
ComponentGroups(
List(),
List(
extendedComponentGroup("Custom", "Lib", "Vector", "three")
)
)
),
config(
"user",
"Other_Utils",
ComponentGroups(
List(),
List(
extendedComponentGroup("Standard", "Base", "Data.List", "four")
)
)
)
)
resolver.run(testPackages) shouldEqual Vector(
libraryComponentGroup("user", "Unnamed", "Main", "main"),
libraryComponentGroup("Standard", "Base", "Data.Vector", "one", "two")
)
}
}
}
object ComponentGroupsResolverSpec {
/** Create a new config. */
def config(
namespace: String,
name: String,
componentGroups: ComponentGroups = ComponentGroups.empty
): Config =
Config(
name = name,
namespace = namespace,
version = "0.0.1",
license = "",
authors = Nil,
maintainers = Nil,
edition = None,
preferLocalLibraries = true,
componentGroups = Right(componentGroups)
)
/** Create a new component group. */
def newComponentGroup(
module: String,
exports: String*
): ComponentGroup =
ComponentGroup(
module = ModuleName(module),
color = None,
icon = None,
exports = exports.map(Component(_, None))
)
/** Create a new extended component group. */
def extendedComponentGroup(
extendedLibraryNamespace: String,
extendedLibraryName: String,
extendedModule: String,
exports: String*
): ExtendedComponentGroup =
ExtendedComponentGroup(
module = ModuleReference(
LibraryName(extendedLibraryNamespace, extendedLibraryName),
ModuleName(extendedModule)
),
exports = exports.map(Component(_, None))
)
/** Create a new library component group. */
def libraryComponentGroup(
namespace: String,
name: String,
module: String,
exports: String*
): LibraryComponentGroup =
LibraryComponentGroup(
library = LibraryName(namespace, name),
module = ModuleName(module),
color = None,
icon = None,
exports = exports.map(Component(_, None))
)
}

View File

@ -0,0 +1,145 @@
package org.enso.languageserver.libraries
import io.circe.DecodingFailure
import org.enso.editions.LibraryName
import org.enso.pkg.{ComponentGroups, Config, ModuleName, ModuleReference}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
class ComponentGroupsValidatorSpec extends AnyWordSpec with Matchers {
import ComponentGroupsValidator._
import ComponentGroupsValidatorSpec._
import ComponentGroupsResolverSpec._
"ComponentGroupsValidator" should {
"validate invalid component groups" in {
val validator = new ComponentGroupsValidator
val testPackages = Vector(
config("Foo", "Bar"),
config(
"Foo",
"Baz",
ComponentGroups(List(newComponentGroup("Mod1", "one", "two")), List())
),
configError(
"Foo",
"Quux",
"Error message"
)
)
validator.validate(testPackages) shouldEqual testPackages.map { config =>
config.componentGroups match {
case Right(_) =>
Right(config)
case Left(error) =>
Left(
ValidationError.InvalidComponentGroups(
libraryName(config),
error.getMessage()
)
)
}
}
}
"validate duplicate component groups" in {
val validator = new ComponentGroupsValidator
val testPackages = Vector(
config(
"Foo",
"Bar",
ComponentGroups(List(newComponentGroup("Mod1", "a", "b")), List())
),
config(
"Foo",
"Bar",
ComponentGroups(List(newComponentGroup("Mod1", "one", "two")), List())
),
config(
"Baz",
"Quux",
ComponentGroups(List(newComponentGroup("Mod1", "one", "two")), List())
)
)
validator
.validate(testPackages) shouldEqual Vector(
Right(testPackages(0)),
Left(
ValidationError.DuplicatedComponentGroup(
libraryName(testPackages(1)),
ModuleReference(LibraryName("Foo", "Bar"), ModuleName("Mod1"))
)
),
Right(testPackages(2))
)
}
"validate non-existent extensions" in {
val validator = new ComponentGroupsValidator
val testPackages = Vector(
config(
"Foo",
"Bar",
ComponentGroups(List(newComponentGroup("Mod1", "a", "b")), List())
),
config(
"Foo",
"Baz",
ComponentGroups(
List(),
List(extendedComponentGroup("Foo", "Bar", "Mod1", "c", "d"))
)
),
config(
"Baz",
"Quux",
ComponentGroups(
List(),
List(extendedComponentGroup("Foo", "Baz", "Mod1", "quuux"))
)
)
)
validator
.validate(testPackages) shouldEqual Vector(
Right(testPackages(0)),
Right(testPackages(1)),
Left(
ValidationError.ComponentGroupExtendsNothing(
libraryName(testPackages(2)),
ModuleReference(LibraryName("Foo", "Baz"), ModuleName("Mod1"))
)
)
)
}
}
}
object ComponentGroupsValidatorSpec {
/** Create a new config with containing a component groups error. */
def configError(
namespace: String,
name: String,
message: String
): Config =
Config(
name = name,
namespace = namespace,
version = "0.0.1",
license = "",
authors = Nil,
maintainers = Nil,
edition = None,
preferLocalLibraries = true,
componentGroups = Left(DecodingFailure.apply(message, List()))
)
/** Create a library name from config. */
def libraryName(config: Config): LibraryName =
LibraryName(config.namespace, config.name)
}

View File

@ -13,7 +13,19 @@ import org.enso.librarymanager.published.repository.{
ExampleRepository, ExampleRepository,
LibraryManifest LibraryManifest
} }
import org.enso.pkg.{Contact, PackageManager} import org.enso.pkg.{
Component,
ComponentGroup,
ComponentGroups,
Config,
Contact,
ExtendedComponentGroup,
ModuleName,
ModuleReference,
Package,
PackageManager,
Shortcut
}
import org.enso.yaml.YamlHelper import org.enso.yaml.YamlHelper
import java.nio.file.Files import java.nio.file.Files
@ -215,6 +227,96 @@ class LibrariesTest extends BaseServerTest {
""") """)
} }
"get the package config" in {
val client = getInitialisedWsClient()
val testComponentGroups = ComponentGroups(
newGroups = List(
ComponentGroup(
module = ModuleName("Foo"),
color = Some("#32a852"),
icon = None,
exports = Seq(
Component("foo", Some(Shortcut("abc"))),
Component("bar", None)
)
)
),
extendedGroups = List(
ExtendedComponentGroup(
module = ModuleReference(
LibraryName("Standard", "Base"),
ModuleName("Data")
),
exports = List(
Component("bar", None)
)
)
)
)
client.send(json"""
{ "jsonrpc": "2.0",
"method": "library/create",
"id": 0,
"params": {
"namespace": "user",
"name": "Get_Package_Test_Lib",
"authors": [],
"maintainers": [],
"license": ""
}
}
""")
client.expectJson(json"""
{ "jsonrpc": "2.0",
"id": 0,
"result": null
}
""")
val libraryRoot = getTestDirectory
.resolve("test_home")
.resolve("libraries")
.resolve("user")
.resolve("Get_Package_Test_Lib")
val packageFile = libraryRoot.resolve(Package.configFileName)
val packageConfig =
YamlHelper
.load[Config](packageFile)
.get
.copy(
componentGroups = Right(testComponentGroups)
)
Files.writeString(packageFile, packageConfig.toYaml)
client.send(json"""
{ "jsonrpc": "2.0",
"method": "library/getPackage",
"id": 1,
"params": {
"namespace": "user",
"name": "Get_Package_Test_Lib",
"version": {
"type": "LocalLibraryVersion"
}
}
}
""")
val response = client.expectSomeJson()
response.hcursor
.downField("result")
.downField("license")
.as[Option[String]]
.rightValue shouldEqual None
response.hcursor
.downField("result")
.downField("componentGroups")
.as[ComponentGroups]
.rightValue shouldEqual testComponentGroups
}
"create, publish a library and fetch its manifest from the server" in { "create, publish a library and fetch its manifest from the server" in {
val client = getInitialisedWsClient() val client = getInitialisedWsClient()
client.send(json""" client.send(json"""
@ -475,14 +577,16 @@ class LibrariesTest extends BaseServerTest {
.value .value
val pkg = val pkg =
PackageManager.Default.loadPackage(cachedLibraryRoot.toFile).get PackageManager.Default
.loadPackage(cachedLibraryRoot.location.toFile)
.get
pkg.name shouldEqual "Bar" pkg.name shouldEqual "Bar"
pkg.listSources.map( pkg.listSources.map(
_.file.getName _.file.getName
) should contain theSameElementsAs Seq("Main.enso") ) should contain theSameElementsAs Seq("Main.enso")
assert( assert(
Files.exists(cachedLibraryRoot.resolve(LibraryManifest.filename)), Files.exists(cachedLibraryRoot / LibraryManifest.filename),
"The manifest file of a downloaded library should be saved in the cache too." "The manifest file of a downloaded library should be saved in the cache too."
) )
} }
@ -609,6 +713,55 @@ class LibrariesTest extends BaseServerTest {
} }
} }
"editions/listDefinedComponents" should {
"include expected components in the list" in {
val client = getInitialisedWsClient()
client.send(json"""
{ "jsonrpc": "2.0",
"method": "editions/listDefinedComponents",
"id": 0,
"params": {
"edition": {
"type": "CurrentProjectEdition"
}
}
}
""")
client.expectJson(json"""
{ "jsonrpc": "2.0",
"id": 0,
"result": {
"availableComponents" : [ ]
}
}
""")
val currentEditionName = buildinfo.Info.currentEdition
client.send(json"""
{ "jsonrpc": "2.0",
"method": "editions/listDefinedComponents",
"id": 0,
"params": {
"edition": {
"type": "NamedEdition",
"editionName": $currentEditionName
}
}
}
""")
client.expectJson(json"""
{ "jsonrpc": "2.0",
"id": 0,
"result": {
"availableComponents" : [ ]
}
}
""")
}
}
"editions/resolve" should { "editions/resolve" should {
"resolve the engine version associated with an edition" in { "resolve the engine version associated with an edition" in {
val currentVersion = buildinfo.Info.ensoVersion val currentVersion = buildinfo.Info.ensoVersion

View File

@ -10,6 +10,7 @@ import org.enso.interpreter.instrument.NotificationHandler
import org.enso.interpreter.runtime.builtin.Builtins import org.enso.interpreter.runtime.builtin.Builtins
import org.enso.interpreter.runtime.util.TruffleFileSystem import org.enso.interpreter.runtime.util.TruffleFileSystem
import org.enso.interpreter.runtime.{Context, Module} import org.enso.interpreter.runtime.{Context, Module}
import org.enso.librarymanager.resolved.LibraryRoot
import org.enso.librarymanager.{ import org.enso.librarymanager.{
DefaultLibraryProvider, DefaultLibraryProvider,
ResolvingLibraryProvider ResolvingLibraryProvider
@ -23,6 +24,7 @@ import org.enso.pkg.{
PackageManager, PackageManager,
QualifiedName QualifiedName
} }
import java.nio.file.Path import java.nio.file.Path
import scala.util.Try import scala.util.Try
@ -247,14 +249,14 @@ object PackageRepository {
private def loadPackage( private def loadPackage(
libraryName: LibraryName, libraryName: LibraryName,
libraryVersion: LibraryVersion, libraryVersion: LibraryVersion,
root: Path root: LibraryRoot
): Either[Error, Package[TruffleFile]] = Try { ): Either[Error, Package[TruffleFile]] = Try {
logger.debug( logger.debug(
s"Loading library $libraryName from " + s"Loading library $libraryName from " +
s"[${MaskedPath(root).applyMasking()}]." s"[${MaskedPath(root.location).applyMasking()}]."
) )
val rootFile = context.getEnvironment.getInternalTruffleFile( val rootFile = context.getEnvironment.getInternalTruffleFile(
root.toAbsolutePath.normalize.toString root.location.toAbsolutePath.normalize.toString
) )
val pkg = packageManager.loadPackage(rootFile).get val pkg = packageManager.loadPackage(rootFile).get
registerPackageInternal( registerPackageInternal(
@ -362,7 +364,7 @@ object PackageRepository {
case Right(resolved) => case Right(resolved) =>
logger.info( logger.info(
s"Found library ${resolved.name} @ ${resolved.version} " + s"Found library ${resolved.name} @ ${resolved.version} " +
s"at [${MaskedPath(resolved.location).applyMasking()}]." s"at [${MaskedPath(resolved.root.location).applyMasking()}]."
) )
} }
@ -373,7 +375,7 @@ object PackageRepository {
else else
resolvedLibrary resolvedLibrary
.flatMap { library => .flatMap { library =>
loadPackage(library.name, library.version, library.location) loadPackage(library.name, library.version, library.root)
} }
.flatMap(resolveComponentGroups) .flatMap(resolveComponentGroups)
.left .left

View File

@ -62,8 +62,6 @@ class RuntimeComponentsTest
LibraryName("Standard", "Base"), LibraryName("Standard", "Base"),
ModuleName("Group2") ModuleName("Group2")
), ),
color = None,
icon = None,
exports = List(Component("foo", None)) exports = List(Component("foo", None))
) )
) )

View File

@ -41,13 +41,14 @@ class LibraryDownloadTest
) )
.get .get
} }
val pkg = PackageManager.Default.loadPackage(libPath.toFile).get val pkg =
PackageManager.Default.loadPackage(libPath.location.toFile).get
pkg.name shouldEqual "Bar" pkg.name shouldEqual "Bar"
val sources = pkg.listSources val sources = pkg.listSources
sources should have size 1 sources should have size 1
sources.head.file.getName shouldEqual "Main.enso" sources.head.file.getName shouldEqual "Main.enso"
assert( assert(
Files.notExists(libPath.resolve("LICENSE.md")), Files.notExists(libPath / "LICENSE.md"),
"The license file should not exist as it was not provided " + "The license file should not exist as it was not provided " +
"in the repository." "in the repository."
) )

View File

@ -80,7 +80,9 @@ class LibraryUploadTest
) )
val installedRoot = val installedRoot =
cache.findOrInstallLibrary(libraryName, libraryVersion, repo).get cache.findOrInstallLibrary(libraryName, libraryVersion, repo).get
val pkg = PackageManager.Default.loadPackage(installedRoot.toFile).get val pkg = PackageManager.Default
.loadPackage(installedRoot.location.toFile)
.get
pkg.name shouldEqual libraryName.name pkg.name shouldEqual libraryName.name
val sources = pkg.listSources val sources = pkg.listSources
sources should have size 1 sources should have size 1

View File

@ -1,12 +1,32 @@
package org.enso.librarymanager package org.enso.librarymanager
import org.enso.editions.{LibraryName, LibraryVersion} import org.enso.editions.{LibraryName, LibraryVersion}
import org.enso.librarymanager.resolved.{
FilesystemLibraryReadAccess,
LibraryReadAccess,
LibraryRoot
}
import java.nio.file.Path /** Represents a resolved library that is located somewhere on the filesystem.
*
/** Represents a resolved library that is located somewhere on the filesystem. */ * @param name the library name
* @param version the library version
* @param root the library location on the filesystem
*/
case class ResolvedLibrary( case class ResolvedLibrary(
name: LibraryName, name: LibraryName,
version: LibraryVersion, version: LibraryVersion,
location: Path root: LibraryRoot
) )
object ResolvedLibrary {
/** Extension methods of [[ResolvedLibrary]]. */
implicit class ResolvedLibraryMethods(val resolvedLibrary: ResolvedLibrary)
extends AnyVal {
/** Provides read methods to access the library files. */
def getReadAccess: LibraryReadAccess =
new FilesystemLibraryReadAccess(resolvedLibrary.root)
}
}

View File

@ -8,10 +8,8 @@ import org.enso.librarymanager.published.repository.LibraryManifest
import org.enso.librarymanager.published.repository.RepositoryHelper.RepositoryMethods import org.enso.librarymanager.published.repository.RepositoryHelper.RepositoryMethods
import org.enso.libraryupload.DependencyExtractor import org.enso.libraryupload.DependencyExtractor
import org.enso.pkg.PackageManager import org.enso.pkg.PackageManager
import org.enso.yaml.YamlHelper
import java.io.File import java.io.File
import java.nio.file.Files
import scala.util.Try import scala.util.Try
/** A helper class that allows to find all transitive dependencies of a specific /** A helper class that allows to find all transitive dependencies of a specific
@ -61,7 +59,7 @@ class DependencyResolver(
case LibraryVersion.Local => case LibraryVersion.Local =>
val libraryPath = localLibraryProvider.findLibrary(libraryName) val libraryPath = localLibraryProvider.findLibrary(libraryName)
val libraryPackage = libraryPath.map(path => val libraryPackage = libraryPath.map(path =>
PackageManager.Default.loadPackage(path.toFile).get PackageManager.Default.loadPackage(path.location.toFile).get
) )
val dependencies = libraryPackage match { val dependencies = libraryPackage match {
@ -99,16 +97,11 @@ class DependencyResolver(
): LibraryManifest = { ): LibraryManifest = {
val cachedManifest = publishedLibraryProvider val cachedManifest = publishedLibraryProvider
.findCachedLibrary(libraryName, version.version) .findCachedLibrary(libraryName, version.version)
.flatMap { libraryPath => .flatMap(_.getReadAccess.readManifest().flatMap(_.toOption))
val manifestPath = libraryPath.resolve(LibraryManifest.filename)
if (Files.exists(manifestPath))
YamlHelper.load[LibraryManifest](manifestPath).toOption
else None
}
cachedManifest.getOrElse { cachedManifest.getOrElse {
version.repository version.repository
.accessLibrary(libraryName, version.version) .accessLibrary(libraryName, version.version)
.downloadManifest() .fetchManifest()
.force() .force()
} }
} }

View File

@ -3,9 +3,11 @@ package org.enso.librarymanager.local
import com.typesafe.scalalogging.Logger import com.typesafe.scalalogging.Logger
import org.enso.editions.LibraryName import org.enso.editions.LibraryName
import org.enso.librarymanager.LibraryLocations import org.enso.librarymanager.LibraryLocations
import org.enso.librarymanager.resolved.LibraryRoot
import org.enso.logger.masking.MaskedPath import org.enso.logger.masking.MaskedPath
import java.nio.file.{Files, Path} import java.nio.file.{Files, Path}
import scala.annotation.tailrec import scala.annotation.tailrec
/** A default implementation of [[LocalLibraryProvider]]. */ /** A default implementation of [[LocalLibraryProvider]]. */
@ -15,13 +17,13 @@ class DefaultLocalLibraryProvider(searchPaths: List[Path])
private val logger = Logger[DefaultLocalLibraryProvider] private val logger = Logger[DefaultLocalLibraryProvider]
/** @inheritdoc */ /** @inheritdoc */
override def findLibrary(libraryName: LibraryName): Option[Path] = override def findLibrary(libraryName: LibraryName): Option[LibraryRoot] = {
findLibraryHelper( findLibraryHelper(libraryName, searchPaths)
libraryName, .map(LibraryRoot(_))
searchPaths }
)
/** Searches through the available library paths, checking if any one of them contains the requested library. /** Searches through the available library paths, checking if any one of them
* contains the requested library.
* *
* The first path on the list takes precedence. * The first path on the list takes precedence.
*/ */

View File

@ -2,16 +2,19 @@ package org.enso.librarymanager.local
import org.enso.distribution.FileSystem.PathSyntax import org.enso.distribution.FileSystem.PathSyntax
import org.enso.editions.LibraryName import org.enso.editions.LibraryName
import org.enso.librarymanager.resolved.LibraryRoot
import java.nio.file.Path import java.nio.file.Path
/** A provider for local libraries. */ /** A provider for local libraries. */
trait LocalLibraryProvider { trait LocalLibraryProvider {
/** Returns the path to a local instance of the requested library, if it is /** Find the local library by name.
* available. *
* @param libraryName the library name
* @return the location of the requested library, if it is available.
*/ */
def findLibrary(libraryName: LibraryName): Option[Path] def findLibrary(libraryName: LibraryName): Option[LibraryRoot]
} }
object LocalLibraryProvider { object LocalLibraryProvider {

View File

@ -1,27 +1,24 @@
package org.enso.librarymanager.published package org.enso.librarymanager.published
import nl.gn0s1s.bump.SemVer import nl.gn0s1s.bump.SemVer
import org.enso.editions.{Editions, LibraryName} import org.enso.editions.LibraryName
import org.enso.librarymanager.LibraryResolutionError
import org.enso.librarymanager.published.cache.ReadOnlyLibraryCache import org.enso.librarymanager.published.cache.ReadOnlyLibraryCache
import org.enso.librarymanager.resolved.LibraryRoot
import java.nio.file.Path
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.util.Try
/** A [[PublishedLibraryProvider]] that just provides libraries which are /** A [[PublishedLibraryCache]] that just provides libraries which are
* already available in the cache. * already available in the cache.
*/ */
class CachedLibraryProvider(caches: List[ReadOnlyLibraryCache]) class CachedLibraryProvider(caches: List[ReadOnlyLibraryCache])
extends PublishedLibraryProvider extends PublishedLibraryCache {
with PublishedLibraryCache {
@tailrec @tailrec
private def findCachedHelper( private def findCachedHelper(
libraryName: LibraryName, libraryName: LibraryName,
version: SemVer, version: SemVer,
caches: List[ReadOnlyLibraryCache] caches: List[ReadOnlyLibraryCache]
): Option[Path] = caches match { ): Option[LibraryRoot] = caches match {
case head :: tail => case head :: tail =>
head.findCachedLibrary(libraryName, version) match { head.findCachedLibrary(libraryName, version) match {
case Some(found) => Some(found) case Some(found) => Some(found)
@ -34,21 +31,8 @@ class CachedLibraryProvider(caches: List[ReadOnlyLibraryCache])
override def findCachedLibrary( override def findCachedLibrary(
libraryName: LibraryName, libraryName: LibraryName,
version: SemVer version: SemVer
): Option[Path] = findCachedHelper(libraryName, version, caches) ): Option[LibraryRoot] =
findCachedHelper(libraryName, version, caches)
/** @inheritdoc */
override def findLibrary(
libraryName: LibraryName,
version: SemVer,
recommendedRepository: Editions.Repository
): Try[Path] =
findCachedLibrary(libraryName, version)
.toRight(
LibraryResolutionError(
s"Library [$libraryName:$version] was not found in the cache."
)
)
.toTry
/** @inheritdoc */ /** @inheritdoc */
override def isLibraryCached( override def isLibraryCached(

View File

@ -7,8 +7,8 @@ import org.enso.librarymanager.published.cache.{
LibraryCache, LibraryCache,
ReadOnlyLibraryCache ReadOnlyLibraryCache
} }
import org.enso.librarymanager.resolved.LibraryRoot
import java.nio.file.Path
import scala.util.{Success, Try} import scala.util.{Success, Try}
/** A default implementation of [[PublishedLibraryProvider]] which uses one /** A default implementation of [[PublishedLibraryProvider]] which uses one
@ -18,7 +18,8 @@ import scala.util.{Success, Try}
class DefaultPublishedLibraryProvider( class DefaultPublishedLibraryProvider(
primaryCache: LibraryCache, primaryCache: LibraryCache,
auxiliaryCaches: List[ReadOnlyLibraryCache] auxiliaryCaches: List[ReadOnlyLibraryCache]
) extends CachedLibraryProvider(caches = primaryCache :: auxiliaryCaches) { ) extends CachedLibraryProvider(caches = primaryCache :: auxiliaryCaches)
with PublishedLibraryProvider {
private val logger = Logger[DefaultPublishedLibraryProvider] private val logger = Logger[DefaultPublishedLibraryProvider]
/** @inheritdoc */ /** @inheritdoc */
@ -26,9 +27,9 @@ class DefaultPublishedLibraryProvider(
libraryName: LibraryName, libraryName: LibraryName,
version: SemVer, version: SemVer,
recommendedRepository: Editions.Repository recommendedRepository: Editions.Repository
): Try[Path] = { ): Try[LibraryRoot] = {
val cached = findCachedLibrary(libraryName, version) val cachedLibrary = findCachedLibrary(libraryName, version)
cached.map(Success(_)).getOrElse { cachedLibrary.map(Success(_)).getOrElse {
logger.trace( logger.trace(
s"$libraryName was not found in any caches, it will need to be " + s"$libraryName was not found in any caches, it will need to be " +
s"downloaded." s"downloaded."

View File

@ -4,6 +4,7 @@ import nl.gn0s1s.bump.SemVer
import org.enso.editions.LibraryName import org.enso.editions.LibraryName
import org.enso.librarymanager.LibraryLocations import org.enso.librarymanager.LibraryLocations
import org.enso.librarymanager.published.bundles.LocalReadOnlyRepository import org.enso.librarymanager.published.bundles.LocalReadOnlyRepository
import org.enso.librarymanager.resolved.LibraryRoot
import java.nio.file.Path import java.nio.file.Path
@ -18,7 +19,10 @@ trait PublishedLibraryCache {
def isLibraryCached(libraryName: LibraryName, version: SemVer): Boolean def isLibraryCached(libraryName: LibraryName, version: SemVer): Boolean
/** Tries to locate a cached version of the requested library. */ /** Tries to locate a cached version of the requested library. */
def findCachedLibrary(libraryName: LibraryName, version: SemVer): Option[Path] def findCachedLibrary(
libraryName: LibraryName,
version: SemVer
): Option[LibraryRoot]
} }
object PublishedLibraryCache { object PublishedLibraryCache {

View File

@ -3,8 +3,8 @@ package org.enso.librarymanager.published
import nl.gn0s1s.bump.SemVer import nl.gn0s1s.bump.SemVer
import org.enso.editions.Editions.Repository import org.enso.editions.Editions.Repository
import org.enso.editions.LibraryName import org.enso.editions.LibraryName
import org.enso.librarymanager.resolved.LibraryRoot
import java.nio.file.Path
import scala.util.Try import scala.util.Try
/** A provider of published libraries. /** A provider of published libraries.
@ -24,5 +24,5 @@ trait PublishedLibraryProvider {
libraryName: LibraryName, libraryName: LibraryName,
version: SemVer, version: SemVer,
recommendedRepository: Repository recommendedRepository: Repository
): Try[Path] ): Try[LibraryRoot]
} }

View File

@ -7,6 +7,7 @@ import org.enso.librarymanager.published.cache.{
LibraryCache, LibraryCache,
ReadOnlyLibraryCache ReadOnlyLibraryCache
} }
import org.enso.librarymanager.resolved.LibraryRoot
import org.enso.logger.masking.MaskedPath import org.enso.logger.masking.MaskedPath
import java.nio.file.{Files, Path} import java.nio.file.{Files, Path}
@ -31,13 +32,13 @@ class LocalReadOnlyRepository(root: Path) extends ReadOnlyLibraryCache {
override def findCachedLibrary( override def findCachedLibrary(
libraryName: LibraryName, libraryName: LibraryName,
version: SemVer version: SemVer
): Option[Path] = { ): Option[LibraryRoot] = {
val path = LibraryCache.resolvePath(root, libraryName, version) val path = LibraryCache.resolvePath(root, libraryName, version)
if (Files.exists(path)) { if (Files.exists(path)) {
logger.trace( logger.trace(
s"$libraryName found at [${MaskedPath(path).applyMasking()}]." s"$libraryName found at [${MaskedPath(path).applyMasking()}]."
) )
Some(path) Some(LibraryRoot(path))
} else { } else {
logger.trace( logger.trace(
s"Did not find $libraryName at [${MaskedPath(path).applyMasking()}]." s"Did not find $libraryName at [${MaskedPath(path).applyMasking()}]."

View File

@ -18,11 +18,13 @@ import org.enso.librarymanager.published.repository.RepositoryHelper.{
LibraryAccess, LibraryAccess,
RepositoryMethods RepositoryMethods
} }
import org.enso.librarymanager.resolved.LibraryRoot
import org.enso.logger.masking.MaskedPath import org.enso.logger.masking.MaskedPath
import org.enso.pkg.PackageManager import org.enso.pkg.PackageManager
import org.enso.yaml.YamlHelper import org.enso.yaml.YamlHelper
import java.nio.file.{Files, Path} import java.nio.file.{Files, Path}
import scala.util.control.NonFatal import scala.util.control.NonFatal
import scala.util.{Success, Try} import scala.util.{Success, Try}
@ -50,7 +52,7 @@ class DownloadingLibraryCache(
override def findCachedLibrary( override def findCachedLibrary(
libraryName: LibraryName, libraryName: LibraryName,
version: SemVer version: SemVer
): Option[Path] = { ): Option[LibraryRoot] = {
val path = LibraryCache.resolvePath(cacheRoot, libraryName, version) val path = LibraryCache.resolvePath(cacheRoot, libraryName, version)
resourceManager.withResource( resourceManager.withResource(
lockUserInterface, lockUserInterface,
@ -62,7 +64,7 @@ class DownloadingLibraryCache(
s"Library [$libraryName:$version] found cached at " + s"Library [$libraryName:$version] found cached at " +
s"[${MaskedPath(path).applyMasking()}]." s"[${MaskedPath(path).applyMasking()}]."
) )
Some(path) Some(LibraryRoot(path))
} else None } else None
} }
} }
@ -72,7 +74,7 @@ class DownloadingLibraryCache(
libraryName: LibraryName, libraryName: LibraryName,
version: SemVer, version: SemVer,
recommendedRepository: Editions.Repository recommendedRepository: Editions.Repository
): Try[Path] = { ): Try[LibraryRoot] = {
val cached = findCachedLibrary(libraryName, version) val cached = findCachedLibrary(libraryName, version)
cached match { cached match {
case Some(result) => Success(result) case Some(result) => Success(result)
@ -95,7 +97,7 @@ class DownloadingLibraryCache(
libraryName: LibraryName, libraryName: LibraryName,
version: SemVer, version: SemVer,
recommendedRepository: Editions.Repository recommendedRepository: Editions.Repository
): Try[Path] = Try { ): Try[LibraryRoot] = Try {
logger.trace(s"Trying to install [$libraryName:$version].") logger.trace(s"Trying to install [$libraryName:$version].")
resourceManager.withResource( resourceManager.withResource(
lockUserInterface, lockUserInterface,
@ -108,7 +110,7 @@ class DownloadingLibraryCache(
logger.info( logger.info(
s"Another process has just installed [$libraryName:$version]." s"Another process has just installed [$libraryName:$version]."
) )
cachedLibraryPath LibraryRoot(cachedLibraryPath)
} else { } else {
val access = recommendedRepository.accessLibrary(libraryName, version) val access = recommendedRepository.accessLibrary(libraryName, version)
val manifest = downloadManifest(libraryName, access) val manifest = downloadManifest(libraryName, access)
@ -129,7 +131,7 @@ class DownloadingLibraryCache(
destination = cachedLibraryPath destination = cachedLibraryPath
) )
cachedLibraryPath LibraryRoot(cachedLibraryPath)
} catch { } catch {
case NonFatal(exception) => case NonFatal(exception) =>
logger.error( logger.error(
@ -149,7 +151,7 @@ class DownloadingLibraryCache(
libraryName: LibraryName, libraryName: LibraryName,
access: LibraryAccess access: LibraryAccess
): LibraryManifest = { ): LibraryManifest = {
val manifestDownload = access.downloadManifest() val manifestDownload = access.fetchManifest()
progressReporter.trackProgress( progressReporter.trackProgress(
s"Downloading library manifest of [$libraryName].", s"Downloading library manifest of [$libraryName].",
manifestDownload manifestDownload

View File

@ -2,8 +2,10 @@ package org.enso.librarymanager.published.cache
import nl.gn0s1s.bump.SemVer import nl.gn0s1s.bump.SemVer
import org.enso.editions.{Editions, LibraryName, LibraryVersion} import org.enso.editions.{Editions, LibraryName, LibraryVersion}
import org.enso.librarymanager.resolved.LibraryRoot
import java.nio.file.Path import java.nio.file.Path
import scala.util.Try import scala.util.Try
/** A library cache that is also capable of downloading missing libraries (which /** A library cache that is also capable of downloading missing libraries (which
@ -26,7 +28,7 @@ trait LibraryCache extends ReadOnlyLibraryCache {
override def findCachedLibrary( override def findCachedLibrary(
libraryName: LibraryName, libraryName: LibraryName,
version: SemVer version: SemVer
): Option[Path] ): Option[LibraryRoot]
/** If the cache contains the library, it is returned immediately, otherwise, /** If the cache contains the library, it is returned immediately, otherwise,
* it tries to download the missing library. * it tries to download the missing library.
@ -46,7 +48,7 @@ trait LibraryCache extends ReadOnlyLibraryCache {
libraryName: LibraryName, libraryName: LibraryName,
version: SemVer, version: SemVer,
recommendedRepository: Editions.Repository recommendedRepository: Editions.Repository
): Try[Path] ): Try[LibraryRoot]
/** Ensures that the given library and all of its dependencies are installed. /** Ensures that the given library and all of its dependencies are installed.
* *

View File

@ -2,8 +2,7 @@ package org.enso.librarymanager.published.cache
import nl.gn0s1s.bump.SemVer import nl.gn0s1s.bump.SemVer
import org.enso.editions.LibraryName import org.enso.editions.LibraryName
import org.enso.librarymanager.resolved.LibraryRoot
import java.nio.file.Path
/** A read-only cache may contain some pre-defined set of libraries, but it does /** A read-only cache may contain some pre-defined set of libraries, but it does
* not necessarily install any additional libraries. * not necessarily install any additional libraries.
@ -15,5 +14,8 @@ trait ReadOnlyLibraryCache {
/** Locates the library in the cache and returns the path to its root if it /** Locates the library in the cache and returns the path to its root if it
* has been found. * has been found.
*/ */
def findCachedLibrary(libraryName: LibraryName, version: SemVer): Option[Path] def findCachedLibrary(
libraryName: LibraryName,
version: SemVer
): Option[LibraryRoot]
} }

View File

@ -6,10 +6,10 @@ import org.enso.distribution.FileSystem.PathSyntax
import org.enso.downloader.http.{HTTPDownload, URIBuilder} import org.enso.downloader.http.{HTTPDownload, URIBuilder}
import org.enso.editions.Editions.Repository import org.enso.editions.Editions.Repository
import org.enso.editions.LibraryName import org.enso.editions.LibraryName
import org.enso.pkg.Package import org.enso.pkg.{Config, Package}
import org.enso.yaml.YamlHelper import org.enso.yaml.YamlHelper
import java.nio.file.Path import java.nio.file.Path
import scala.util.Failure import scala.util.Failure
/** A class that manages the HTTP API of the Library Repository. /** A class that manages the HTTP API of the Library Repository.
@ -52,15 +52,15 @@ object RepositoryHelper {
libraryRoot: URIBuilder libraryRoot: URIBuilder
) { ) {
/** Downloads and parses the manifest file. /** Fetches the contents of manifest file and parses it.
* *
* If the repository responds with 404 status code, it returns a special * If the repository responds with 404 status code, it returns a special
* [[LibraryNotFoundException]] indicating that the repository does not * [[LibraryNotFoundException]] indicating that the repository does not
* provide that library. Any other failures are indicated with the more * provide that library. Any other failures are indicated with the more
* generic [[LibraryDownloadFailure]]. * generic [[LibraryDownloadFailure]].
*/ */
def downloadManifest(): TaskProgress[LibraryManifest] = { def fetchManifest(): TaskProgress[LibraryManifest] = {
val url = (libraryRoot / manifestFilename).build() val url = (libraryRoot / LibraryManifest.filename).build()
HTTPDownload.fetchString(url).flatMap { response => HTTPDownload.fetchString(url).flatMap { response =>
response.statusCode match { response.statusCode match {
case 200 => case 200 =>
@ -80,6 +80,34 @@ object RepositoryHelper {
} }
} }
/** Fetches the contents of package config file and parses it.
*
* If the repository responds with 404 status code, it returns a special
* [[LibraryNotFoundException]] indicating that the repository does not
* provide that library. Any other failures are indicated with the more
* generic [[LibraryDownloadFailure]].
*/
def fetchPackageConfig(): TaskProgress[Config] = {
val url = (libraryRoot / Package.configFileName).build()
HTTPDownload.fetchString(url).flatMap { response =>
response.statusCode match {
case 200 =>
YamlHelper.parseString[Config](response.content).toTry
case 404 =>
Failure(
LibraryNotFoundException(libraryName, version, url.toString)
)
case code =>
Failure(
new LibraryDownloadFailure(
s"Could not download the package config: The repository responded " +
s"with $code status code."
)
)
}
}
}
/** A helper that downloads an artifact to a specific location. */ /** A helper that downloads an artifact to a specific location. */
private def downloadArtifact( private def downloadArtifact(
artifactName: String, artifactName: String,
@ -108,9 +136,6 @@ object RepositoryHelper {
): TaskProgress[Unit] = downloadArtifact(archiveName, destinationDirectory) ): TaskProgress[Unit] = downloadArtifact(archiveName, destinationDirectory)
} }
/** Name of the manifest file. */
val manifestFilename = "manifest.yaml"
/** Name of the attached license file. */ /** Name of the attached license file. */
val licenseFilename = "LICENSE.md" val licenseFilename = "LICENSE.md"

View File

@ -0,0 +1,32 @@
package org.enso.librarymanager.resolved
import org.enso.librarymanager.published.repository.LibraryManifest
import org.enso.pkg.{Config, Package}
import org.enso.yaml.YamlHelper
import java.nio.file.Files
import scala.util.Try
/** Default filesystem read access to libraries.
*
* @param libraryPath the library location on the filesystem
*/
class FilesystemLibraryReadAccess(libraryPath: LibraryRoot)
extends LibraryReadAccess {
/** @inheritdoc */
override def readManifest(): Option[Try[LibraryManifest]] = {
val manifestPath = libraryPath.location.resolve(LibraryManifest.filename)
Option.when(Files.exists(manifestPath)) {
YamlHelper.load[LibraryManifest](manifestPath)
}
}
/** @inheritdoc */
override def readPackage(): Try[Config] = {
val configPath = libraryPath.location.resolve(Package.configFileName)
YamlHelper.load[Config](configPath)
}
}

View File

@ -0,0 +1,22 @@
package org.enso.librarymanager.resolved
import org.enso.librarymanager.published.repository.LibraryManifest
import org.enso.pkg.Config
import scala.util.Try
/** Base trait allowing to read the library files on a filesystem. */
trait LibraryReadAccess {
/** Read the library manifest file.
*
* @return the library manifest, if the manifest file exists and `None` otherwise.
*/
def readManifest(): Option[Try[LibraryManifest]]
/** Read the library package config.
*
* @return the parsed library config.
*/
def readPackage(): Try[Config]
}

View File

@ -0,0 +1,28 @@
package org.enso.librarymanager.resolved
import java.nio.file.Path
/** The path to the library on a filesystem.
*
* @param location the library location on a filesystem
*/
case class LibraryRoot(location: Path)
object LibraryRoot {
/** Extension methods of [[LibraryRoot]]. */
implicit class LibraryRootMethods(val libraryPath: LibraryRoot)
extends AnyVal {
/** Get the read access to the library files. */
def getReadAccess: LibraryReadAccess =
new FilesystemLibraryReadAccess(libraryPath)
}
/** Syntax allowing to write nested paths in a more readable and concise way.
*/
implicit class LibraryRootSyntax(val libraryRoot: LibraryRoot)
extends AnyVal {
def /(other: String): Path = libraryRoot.location.resolve(other)
def /(other: Path): Path = libraryRoot.location.resolve(other)
}
}

View File

@ -4,6 +4,7 @@ import nl.gn0s1s.bump.SemVer
import org.enso.editions.Editions.Repository import org.enso.editions.Editions.Repository
import org.enso.editions.{Editions, LibraryName, LibraryVersion} import org.enso.editions.{Editions, LibraryName, LibraryVersion}
import org.enso.librarymanager.local.LocalLibraryProvider import org.enso.librarymanager.local.LocalLibraryProvider
import org.enso.librarymanager.resolved.LibraryRoot
import org.enso.testkit.EitherValue import org.enso.testkit.EitherValue
import org.scalatest.Inside import org.scalatest.Inside
import org.scalatest.matchers.should.Matchers import org.scalatest.matchers.should.Matchers
@ -57,8 +58,14 @@ class LibraryResolverSpec
case class FakeLocalLibraryProvider(fixtures: Map[LibraryName, Path]) case class FakeLocalLibraryProvider(fixtures: Map[LibraryName, Path])
extends LocalLibraryProvider { extends LocalLibraryProvider {
override def findLibrary(libraryName: LibraryName): Option[Path] =
fixtures.get(libraryName) /** @inheritdoc */
override def findLibrary(
libraryName: LibraryName
): Option[LibraryRoot] =
fixtures
.get(libraryName)
.map(LibraryRoot(_))
} }
val localLibraries = Map( val localLibraries = Map(

View File

@ -102,14 +102,10 @@ object ComponentGroup {
/** The definition of a component group that extends an existing one. /** The definition of a component group that extends an existing one.
* *
* @param module the reference to the extended component group * @param module the reference to the extended component group
* @param color the component group color
* @param icon the component group icon
* @param exports the list of components provided by this component group * @param exports the list of components provided by this component group
*/ */
case class ExtendedComponentGroup( case class ExtendedComponentGroup(
module: ModuleReference, module: ModuleReference,
color: Option[String],
icon: Option[String],
exports: Seq[Component] exports: Seq[Component]
) )
object ExtendedComponentGroup { object ExtendedComponentGroup {
@ -117,22 +113,17 @@ object ExtendedComponentGroup {
/** Fields for use when serializing the [[ExtendedComponentGroup]]. */ /** Fields for use when serializing the [[ExtendedComponentGroup]]. */
private object Fields { private object Fields {
val Module = "module" val Module = "module"
val Color = "color"
val Icon = "icon"
val Exports = "exports" val Exports = "exports"
} }
/** [[Encoder]] instance for the [[ExtendedComponentGroup]]. */ /** [[Encoder]] instance for the [[ExtendedComponentGroup]]. */
implicit val encoder: Encoder[ExtendedComponentGroup] = { implicit val encoder: Encoder[ExtendedComponentGroup] = {
extendedComponentGroup => extendedComponentGroup =>
val color = extendedComponentGroup.color.map(Fields.Color -> _.asJson)
val icon = extendedComponentGroup.icon.map(Fields.Icon -> _.asJson)
val exports = Option.unless(extendedComponentGroup.exports.isEmpty)( val exports = Option.unless(extendedComponentGroup.exports.isEmpty)(
Fields.Exports -> extendedComponentGroup.exports.asJson Fields.Exports -> extendedComponentGroup.exports.asJson
) )
Json.obj( Json.obj(
(Fields.Module -> extendedComponentGroup.module.asJson) +: (Fields.Module -> extendedComponentGroup.module.asJson) +: exports.toSeq: _*
(color.toSeq ++ icon.toSeq ++ exports.toSeq): _*
) )
} }
@ -145,10 +136,8 @@ object ExtendedComponentGroup {
Fields.Module, Fields.Module,
json json
) )
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[Component]](Fields.Exports)(List())
} yield ExtendedComponentGroup(reference, color, icon, exports) } yield ExtendedComponentGroup(reference, exports)
} }
} }
@ -219,7 +208,18 @@ object Shortcut {
case class ModuleReference( case class ModuleReference(
libraryName: LibraryName, libraryName: LibraryName,
moduleName: ModuleName moduleName: ModuleName
) ) {
/** The qualified name of the library consists of its prefix and name
* separated with a dot.
*/
def qualifiedName: String =
s"$libraryName${LibraryName.separator}${moduleName.name}"
/** @inheritdoc */
override def toString: String = qualifiedName
}
object ModuleReference { object ModuleReference {
private def toModuleString(moduleReference: ModuleReference): String = { private def toModuleString(moduleReference: ModuleReference): String = {

View File

@ -134,8 +134,6 @@ class ConfigSpec
LibraryName("Standard", "Base"), LibraryName("Standard", "Base"),
ModuleName("Group 2") ModuleName("Group 2")
), ),
color = None,
icon = None,
exports = List(Component("bax", None)) exports = List(Component("bax", None))
) )
) )