diff --git a/RELEASES.md b/RELEASES.md index d076eb0379ed..eadf671e7717 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -12,6 +12,9 @@ - Updated to [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)). +- 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 diff --git a/docs/language-server/protocol-language-server.md b/docs/language-server/protocol-language-server.md index edf4ea06ea3c..79a147d76a7f 100644 --- a/docs/language-server/protocol-language-server.md +++ b/docs/language-server/protocol-language-server.md @@ -62,6 +62,12 @@ transport formats, please look [here](./protocol-architecture). - [`LibraryVersion`](#libraryversion) - [`Contact`](#contact) - [`EditionReference`](#editionreference) + - [`ComponentGroups`](#componentgroups) + - [`ComponentGroup`](#componentgroup) + - [`ExtendedComponentGroup`](#extendedcomponentgroup) + - [`ModuleReference`](#modulereference) + - [`Component`](#component) + - [`LibraryComponentGroup`](#librarycomponentgroup) - [Connection Management](#connection-management) - [`session/initProtocolConnection`](#sessioninitprotocolconnection) - [`session/initBinaryConnection`](#sessioninitbinaryconnection) @@ -156,10 +162,12 @@ transport formats, please look [here](./protocol-architecture). - [`editions/setProjectParentEdition`](#editionssetprojectparentedition) - [`editions/setProjectLocalLibrariesPreference`](#editionssetprojectlocallibrariespreference) - [`editions/listDefinedLibraries`](#editionslistdefinedlibraries) + - [`editions/listDefinedComponents`](#editionslistdefinedcomponents) - [`library/listLocal`](#librarylistlocal) - [`library/create`](#librarycreate) - [`library/getMetadata`](#librarygetmetadata) - [`library/setMetadata`](#librarysetmetadata) + - [`library/getPackage`](#librarygetpackage) - [`library/publish`](#librarypublish) - [`library/preinstall`](#librarypreinstall) - [Errors](#errors-75) @@ -204,6 +212,7 @@ transport formats, please look [here](./protocol-architecture). - [`LibraryNotResolved`](#librarynotresolved) - [`InvalidLibraryName`](#invalidlibraryname) - [`DependencyDiscoveryError`](#dependencydiscoveryerror) + - [`InvalidSemverVersion`](#invalidsemverversion) @@ -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 + * ., 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 + * ., 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 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 +- [`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 edition, or an edition referenced in one of its parents, could not be found. - [`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 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. @@ -4446,6 +4592,47 @@ null; - [`FileSystemError`](#filesystemerror) to signal a generic, unrecoverable 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` 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: ." } ``` + +### `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" : "[] is not a valid semver version.", + "payload" : { + "version" : "" + } +} +``` diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/ComponentGroupsResolver.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/ComponentGroupsResolver.scala new file mode 100644 index 000000000000..42b0219a3315 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/ComponentGroupsResolver.scala @@ -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 +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/ComponentGroupsValidator.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/ComponentGroupsValidator.scala new file mode 100644 index 000000000000..f77b5a75b06e --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/ComponentGroupsValidator.scala @@ -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 + } +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LibraryApi.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LibraryApi.scala index a8840086a26a..1ed43825c09c 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LibraryApi.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LibraryApi.scala @@ -1,10 +1,10 @@ package org.enso.languageserver.libraries -import io.circe.Json +import io.circe.{Json, JsonObject} import io.circe.literal.JsonStringContext import org.enso.editions.{LibraryName, LibraryVersion} import org.enso.jsonrpc.{Error, HasParams, HasResult, Method, Unused} -import org.enso.pkg.Contact +import org.enso.pkg.{ComponentGroups, Contact} object LibraryApi { 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 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 class Params( @@ -256,4 +295,12 @@ object LibraryApi { 8010, 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 }""" + ) + } } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LibraryComponentGroup.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LibraryComponentGroup.scala new file mode 100644 index 000000000000..9eba8be26370 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LibraryComponentGroup.scala @@ -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) + } + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManager.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManager.scala index de132c261d9a..18311442a587 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManager.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManager.scala @@ -2,7 +2,6 @@ package org.enso.languageserver.libraries import akka.actor.Props import com.typesafe.scalalogging.LazyLogging -import org.enso.distribution.FileSystem.PathSyntax import org.enso.distribution.{DistributionManager, FileSystem} import org.enso.editions.{Editions, LibraryName} 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.pkg.validation.NameValidation -import org.enso.pkg.{Contact, PackageManager} +import org.enso.pkg.{Config, Contact, Package, PackageManager} import org.enso.yaml.YamlHelper import java.io.File import java.nio.file.{Files, Path} + import scala.util.{Success, Try} /** An Actor that manages local libraries. */ @@ -41,6 +41,8 @@ class LocalLibraryManager( tagLine = request.tagLine ) ) + case GetPackage(libraryName) => + startRequest(getPackage(libraryName)) case ListLocalLibraries => startRequest(listLocalLibraries()) case Create(libraryName, authors, maintainers, license) => @@ -190,6 +192,28 @@ class LocalLibraryManager( _ = saveManifest(manifestPath, updatedManifest) } 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. * * If the file does not exist, an empty manifest is returned. Any other @@ -206,6 +230,10 @@ class LocalLibraryManager( manifest: LibraryManifest ): 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 * config. */ diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManagerFailureMapper.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManagerFailureMapper.scala new file mode 100644 index 000000000000..5bb0e696c2ca --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManagerFailureMapper.scala @@ -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) + } +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManagerProtocol.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManagerProtocol.scala index 7a84c16ededf..050ad52a80f1 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManagerProtocol.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/LocalLibraryManagerProtocol.scala @@ -1,9 +1,9 @@ package org.enso.languageserver.libraries +import io.circe.JsonObject import org.enso.editions.LibraryName -import org.enso.pkg.Contact - -import java.nio.file.Path +import org.enso.librarymanager.resolved.LibraryRoot +import org.enso.pkg.{ComponentGroups, Contact} object LocalLibraryManagerProtocol { @@ -26,6 +26,16 @@ object LocalLibraryManagerProtocol { tagLine: Option[String] ) 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. */ case object ListLocalLibraries extends Request @@ -44,7 +54,7 @@ object LocalLibraryManagerProtocol { case class FindLibrary(libraryName: LibraryName) extends Request /** 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 * libraries. @@ -59,4 +69,14 @@ object LocalLibraryManagerProtocol { * Sent as a reply to [[Create]] and [[SetMetadata]]. */ 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 } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsListDefinedComponentsHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsListDefinedComponentsHandler.scala new file mode 100644 index 000000000000..4bfdbf196b91 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/EditionsListDefinedComponentsHandler.scala @@ -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 + ) + ) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryGetMetadataHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryGetMetadataHandler.scala index 747450c9b13f..1ce49c9e8159 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryGetMetadataHandler.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryGetMetadataHandler.scala @@ -1,6 +1,8 @@ package org.enso.languageserver.libraries.handler import akka.actor.{Actor, ActorRef, Cancellable, Props, Status} + +import scala.util.{Success, Try} import akka.pattern.pipe import com.typesafe.scalalogging.LazyLogging 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.{ 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 -/** A request handler for the `library/create` endpoint. +/** A request handler for the `library/getMetadata` endpoint. * * @param timeout request timeout * @param localLibraryManager reference to the local library manager actor + * @param publishedLibraryCache the cache of published libraries */ class LibraryGetMetadataHandler( timeout: FiniteDuration, - localLibraryManager: ActorRef + localLibraryManager: ActorRef, + publishedLibraryCache: PublishedLibraryCache ) extends Actor with LazyLogging with UnhandledLogging { @@ -48,11 +54,18 @@ class LibraryGetMetadataHandler( libraryName ) case LibraryEntry.PublishedLibraryVersion(version, repositoryUrl) => - fetchPublishedMetadata( - libraryName, - version, - repositoryUrl - ) pipeTo self + SemVer(version) match { + case Some(semVerVersion) => + getOrFetchPublishedMetadata( + libraryName, + semVerVersion, + repositoryUrl + ) pipeTo self + case None => + self ! LocalLibraryManagerProtocol.InvalidSemverVersionError( + version + ) + } } val cancellable = @@ -82,33 +95,59 @@ class LibraryGetMetadataHandler( 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) } - // TODO [RW] Once the manifests of downloaded libraries are being cached, - // it may be worth to try resolving the local cache first to avoid - // downloading the manifest again. This should be done before the issues - // #1772 or #1775 are completed. + private def getOrFetchPublishedMetadata( + libraryName: LibraryName, + version: SemVer, + 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( libraryName: LibraryName, - version: String, + version: SemVer, repositoryUrl: String ): 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) - .accessLibrary(libraryName, semver) - .downloadManifest() + .accessLibrary(libraryName, version) + .fetchManifest() .toFuture } yield LocalLibraryManagerProtocol.GetMetadataResponse( description = manifest.description, @@ -122,7 +161,18 @@ object LibraryGetMetadataHandler { * * @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): Props = - Props(new LibraryGetMetadataHandler(timeout, localLibraryManager)) + def props( + timeout: FiniteDuration, + localLibraryManager: ActorRef, + publishedLibraryCache: PublishedLibraryCache + ): Props = + Props( + new LibraryGetMetadataHandler( + timeout, + localLibraryManager, + publishedLibraryCache + ) + ) } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryGetPackageHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryGetPackageHandler.scala new file mode 100644 index 000000000000..f410046a3e77 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryGetPackageHandler.scala @@ -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 + ) + ) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPublishHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPublishHandler.scala index 358bf61ee37e..265bd0cda400 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPublishHandler.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/libraries/handler/LibraryPublishHandler.scala @@ -107,7 +107,7 @@ class LibraryPublishHandler( new CompilerBasedDependencyExtractor(logLevel) LibraryUploader(dependencyExtractor) .uploadLibrary( - libraryRoot, + libraryRoot.location, uploadUrl, token, progressReporter diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala index faf8beaf0f62..ed43aacd0570 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala @@ -73,6 +73,7 @@ import org.enso.polyglot.runtime.Runtime.Api import org.enso.polyglot.runtime.Runtime.Api.ProgressNotification import java.util.UUID + import scala.concurrent.duration._ /** An actor handling communications between a single client and the language @@ -506,18 +507,33 @@ class JsonConnectionController( .props(requestTimeout, projectSettingsManager), EditionsSetLocalLibrariesPreference -> EditionsSetProjectLocalLibrariesPreferenceHandler .props(requestTimeout, projectSettingsManager), + EditionsListDefinedComponents -> EditionsListDefinedComponentsHandler + .props( + libraryConfig.editionReferenceResolver, + libraryConfig.localLibraryProvider, + libraryConfig.publishedLibraryCache + ), LibraryCreate -> LibraryCreateHandler .props(requestTimeout, libraryConfig.localLibraryManager), LibraryListLocal -> LibraryListLocalHandler .props(requestTimeout, libraryConfig.localLibraryManager), LibraryGetMetadata -> LibraryGetMetadataHandler - .props(requestTimeout, libraryConfig.localLibraryManager), + .props( + requestTimeout, + libraryConfig.localLibraryManager, + libraryConfig.publishedLibraryCache + ), LibraryPreinstall -> LibraryPreinstallHandler .props(libraryConfig.editionReferenceResolver, libraryConfig), LibraryPublish -> LibraryPublishHandler .props(requestTimeout, libraryConfig.localLibraryManager), LibrarySetMetadata -> LibrarySetMetadataHandler - .props(requestTimeout, libraryConfig.localLibraryManager) + .props(requestTimeout, libraryConfig.localLibraryManager), + LibraryGetPackage -> LibraryGetPackageHandler.props( + requestTimeout, + libraryConfig.localLibraryManager, + libraryConfig.publishedLibraryCache + ) ) } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala index 462a04181954..7111cf4ed883 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonRpc.scala @@ -76,10 +76,12 @@ object JsonRpc { .registerRequest(EditionsSetParentEdition) .registerRequest(EditionsSetLocalLibrariesPreference) .registerRequest(EditionsListDefinedLibraries) + .registerRequest(EditionsListDefinedComponents) .registerRequest(LibraryListLocal) .registerRequest(LibraryCreate) .registerRequest(LibraryGetMetadata) .registerRequest(LibrarySetMetadata) + .registerRequest(LibraryGetPackage) .registerRequest(LibraryPublish) .registerRequest(LibraryPreinstall) .registerNotification(TaskStarted) diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/libraries/ComponentGroupsResolverSpec.scala b/engine/language-server/src/test/scala/org/enso/languageserver/libraries/ComponentGroupsResolverSpec.scala new file mode 100644 index 000000000000..4b0cd5017c3b --- /dev/null +++ b/engine/language-server/src/test/scala/org/enso/languageserver/libraries/ComponentGroupsResolverSpec.scala @@ -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)) + ) +} diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/libraries/ComponentGroupsValidatorSpec.scala b/engine/language-server/src/test/scala/org/enso/languageserver/libraries/ComponentGroupsValidatorSpec.scala new file mode 100644 index 000000000000..c8ac3b60f9c0 --- /dev/null +++ b/engine/language-server/src/test/scala/org/enso/languageserver/libraries/ComponentGroupsValidatorSpec.scala @@ -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) +} diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/LibrariesTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/LibrariesTest.scala index e908669030ee..cdfa98b78268 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/LibrariesTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/LibrariesTest.scala @@ -13,7 +13,19 @@ import org.enso.librarymanager.published.repository.{ ExampleRepository, 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 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 { val client = getInitialisedWsClient() client.send(json""" @@ -475,14 +577,16 @@ class LibrariesTest extends BaseServerTest { .value val pkg = - PackageManager.Default.loadPackage(cachedLibraryRoot.toFile).get + PackageManager.Default + .loadPackage(cachedLibraryRoot.location.toFile) + .get pkg.name shouldEqual "Bar" pkg.listSources.map( _.file.getName ) should contain theSameElementsAs Seq("Main.enso") 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." ) } @@ -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 { "resolve the engine version associated with an edition" in { val currentVersion = buildinfo.Info.ensoVersion diff --git a/engine/runtime/src/main/scala/org/enso/compiler/PackageRepository.scala b/engine/runtime/src/main/scala/org/enso/compiler/PackageRepository.scala index ad43d3d44cd4..57b2910ab15f 100644 --- a/engine/runtime/src/main/scala/org/enso/compiler/PackageRepository.scala +++ b/engine/runtime/src/main/scala/org/enso/compiler/PackageRepository.scala @@ -10,6 +10,7 @@ import org.enso.interpreter.instrument.NotificationHandler import org.enso.interpreter.runtime.builtin.Builtins import org.enso.interpreter.runtime.util.TruffleFileSystem import org.enso.interpreter.runtime.{Context, Module} +import org.enso.librarymanager.resolved.LibraryRoot import org.enso.librarymanager.{ DefaultLibraryProvider, ResolvingLibraryProvider @@ -23,6 +24,7 @@ import org.enso.pkg.{ PackageManager, QualifiedName } + import java.nio.file.Path import scala.util.Try @@ -247,14 +249,14 @@ object PackageRepository { private def loadPackage( libraryName: LibraryName, libraryVersion: LibraryVersion, - root: Path + root: LibraryRoot ): Either[Error, Package[TruffleFile]] = Try { logger.debug( s"Loading library $libraryName from " + - s"[${MaskedPath(root).applyMasking()}]." + s"[${MaskedPath(root.location).applyMasking()}]." ) val rootFile = context.getEnvironment.getInternalTruffleFile( - root.toAbsolutePath.normalize.toString + root.location.toAbsolutePath.normalize.toString ) val pkg = packageManager.loadPackage(rootFile).get registerPackageInternal( @@ -362,7 +364,7 @@ object PackageRepository { case Right(resolved) => logger.info( 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 resolvedLibrary .flatMap { library => - loadPackage(library.name, library.version, library.location) + loadPackage(library.name, library.version, library.root) } .flatMap(resolveComponentGroups) .left diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeComponentsTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeComponentsTest.scala index 20f5b074fc4e..2722afe8aaf6 100644 --- a/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeComponentsTest.scala +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/instrument/RuntimeComponentsTest.scala @@ -62,8 +62,6 @@ class RuntimeComponentsTest LibraryName("Standard", "Base"), ModuleName("Group2") ), - color = None, - icon = None, exports = List(Component("foo", None)) ) ) diff --git a/lib/scala/library-manager-test/src/test/scala/org/enso/librarymanager/published/repository/LibraryDownloadTest.scala b/lib/scala/library-manager-test/src/test/scala/org/enso/librarymanager/published/repository/LibraryDownloadTest.scala index 8901e5ddf6fe..0901b9f48376 100644 --- a/lib/scala/library-manager-test/src/test/scala/org/enso/librarymanager/published/repository/LibraryDownloadTest.scala +++ b/lib/scala/library-manager-test/src/test/scala/org/enso/librarymanager/published/repository/LibraryDownloadTest.scala @@ -41,13 +41,14 @@ class LibraryDownloadTest ) .get } - val pkg = PackageManager.Default.loadPackage(libPath.toFile).get + val pkg = + PackageManager.Default.loadPackage(libPath.location.toFile).get pkg.name shouldEqual "Bar" val sources = pkg.listSources sources should have size 1 sources.head.file.getName shouldEqual "Main.enso" assert( - Files.notExists(libPath.resolve("LICENSE.md")), + Files.notExists(libPath / "LICENSE.md"), "The license file should not exist as it was not provided " + "in the repository." ) diff --git a/lib/scala/library-manager-test/src/test/scala/org/enso/libraryupload/LibraryUploadTest.scala b/lib/scala/library-manager-test/src/test/scala/org/enso/libraryupload/LibraryUploadTest.scala index 9a70c1c97869..4d7e31570852 100644 --- a/lib/scala/library-manager-test/src/test/scala/org/enso/libraryupload/LibraryUploadTest.scala +++ b/lib/scala/library-manager-test/src/test/scala/org/enso/libraryupload/LibraryUploadTest.scala @@ -80,7 +80,9 @@ class LibraryUploadTest ) val installedRoot = 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 val sources = pkg.listSources sources should have size 1 diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/ResolvedLibrary.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/ResolvedLibrary.scala index c4ad51da2872..cd8597346fff 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/ResolvedLibrary.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/ResolvedLibrary.scala @@ -1,12 +1,32 @@ package org.enso.librarymanager 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( name: LibraryName, 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) + } + +} diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/dependencies/DependencyResolver.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/dependencies/DependencyResolver.scala index 388fcffae7c8..af2f3eb97830 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/dependencies/DependencyResolver.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/dependencies/DependencyResolver.scala @@ -8,10 +8,8 @@ import org.enso.librarymanager.published.repository.LibraryManifest import org.enso.librarymanager.published.repository.RepositoryHelper.RepositoryMethods import org.enso.libraryupload.DependencyExtractor import org.enso.pkg.PackageManager -import org.enso.yaml.YamlHelper import java.io.File -import java.nio.file.Files import scala.util.Try /** A helper class that allows to find all transitive dependencies of a specific @@ -61,7 +59,7 @@ class DependencyResolver( case LibraryVersion.Local => val libraryPath = localLibraryProvider.findLibrary(libraryName) val libraryPackage = libraryPath.map(path => - PackageManager.Default.loadPackage(path.toFile).get + PackageManager.Default.loadPackage(path.location.toFile).get ) val dependencies = libraryPackage match { @@ -99,16 +97,11 @@ class DependencyResolver( ): LibraryManifest = { val cachedManifest = publishedLibraryProvider .findCachedLibrary(libraryName, version.version) - .flatMap { libraryPath => - val manifestPath = libraryPath.resolve(LibraryManifest.filename) - if (Files.exists(manifestPath)) - YamlHelper.load[LibraryManifest](manifestPath).toOption - else None - } + .flatMap(_.getReadAccess.readManifest().flatMap(_.toOption)) cachedManifest.getOrElse { version.repository .accessLibrary(libraryName, version.version) - .downloadManifest() + .fetchManifest() .force() } } diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/DefaultLocalLibraryProvider.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/DefaultLocalLibraryProvider.scala index fdec53c8ab9c..78f20f04725e 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/DefaultLocalLibraryProvider.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/DefaultLocalLibraryProvider.scala @@ -3,9 +3,11 @@ package org.enso.librarymanager.local import com.typesafe.scalalogging.Logger import org.enso.editions.LibraryName import org.enso.librarymanager.LibraryLocations +import org.enso.librarymanager.resolved.LibraryRoot import org.enso.logger.masking.MaskedPath import java.nio.file.{Files, Path} + import scala.annotation.tailrec /** A default implementation of [[LocalLibraryProvider]]. */ @@ -15,13 +17,13 @@ class DefaultLocalLibraryProvider(searchPaths: List[Path]) private val logger = Logger[DefaultLocalLibraryProvider] /** @inheritdoc */ - override def findLibrary(libraryName: LibraryName): Option[Path] = - findLibraryHelper( - libraryName, - searchPaths - ) + override def findLibrary(libraryName: LibraryName): Option[LibraryRoot] = { + findLibraryHelper(libraryName, searchPaths) + .map(LibraryRoot(_)) + } - /** 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. */ diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/LocalLibraryProvider.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/LocalLibraryProvider.scala index ba376c431161..6f7b7433356b 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/LocalLibraryProvider.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/local/LocalLibraryProvider.scala @@ -2,16 +2,19 @@ package org.enso.librarymanager.local import org.enso.distribution.FileSystem.PathSyntax import org.enso.editions.LibraryName +import org.enso.librarymanager.resolved.LibraryRoot import java.nio.file.Path /** A provider for local libraries. */ trait LocalLibraryProvider { - /** Returns the path to a local instance of the requested library, if it is - * available. + /** Find the local library by name. + * + * @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 { diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/CachedLibraryProvider.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/CachedLibraryProvider.scala index b73d8d4da564..7ae7ae508bca 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/CachedLibraryProvider.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/CachedLibraryProvider.scala @@ -1,27 +1,24 @@ package org.enso.librarymanager.published import nl.gn0s1s.bump.SemVer -import org.enso.editions.{Editions, LibraryName} -import org.enso.librarymanager.LibraryResolutionError +import org.enso.editions.LibraryName import org.enso.librarymanager.published.cache.ReadOnlyLibraryCache +import org.enso.librarymanager.resolved.LibraryRoot -import java.nio.file.Path 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. */ class CachedLibraryProvider(caches: List[ReadOnlyLibraryCache]) - extends PublishedLibraryProvider - with PublishedLibraryCache { + extends PublishedLibraryCache { @tailrec private def findCachedHelper( libraryName: LibraryName, version: SemVer, caches: List[ReadOnlyLibraryCache] - ): Option[Path] = caches match { + ): Option[LibraryRoot] = caches match { case head :: tail => head.findCachedLibrary(libraryName, version) match { case Some(found) => Some(found) @@ -34,21 +31,8 @@ class CachedLibraryProvider(caches: List[ReadOnlyLibraryCache]) override def findCachedLibrary( libraryName: LibraryName, version: SemVer - ): Option[Path] = 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 + ): Option[LibraryRoot] = + findCachedHelper(libraryName, version, caches) /** @inheritdoc */ override def isLibraryCached( diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/DefaultPublishedLibraryProvider.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/DefaultPublishedLibraryProvider.scala index 06ddddb78846..bc2d189149a3 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/DefaultPublishedLibraryProvider.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/DefaultPublishedLibraryProvider.scala @@ -7,8 +7,8 @@ import org.enso.librarymanager.published.cache.{ LibraryCache, ReadOnlyLibraryCache } +import org.enso.librarymanager.resolved.LibraryRoot -import java.nio.file.Path import scala.util.{Success, Try} /** A default implementation of [[PublishedLibraryProvider]] which uses one @@ -18,7 +18,8 @@ import scala.util.{Success, Try} class DefaultPublishedLibraryProvider( primaryCache: LibraryCache, auxiliaryCaches: List[ReadOnlyLibraryCache] -) extends CachedLibraryProvider(caches = primaryCache :: auxiliaryCaches) { +) extends CachedLibraryProvider(caches = primaryCache :: auxiliaryCaches) + with PublishedLibraryProvider { private val logger = Logger[DefaultPublishedLibraryProvider] /** @inheritdoc */ @@ -26,9 +27,9 @@ class DefaultPublishedLibraryProvider( libraryName: LibraryName, version: SemVer, recommendedRepository: Editions.Repository - ): Try[Path] = { - val cached = findCachedLibrary(libraryName, version) - cached.map(Success(_)).getOrElse { + ): Try[LibraryRoot] = { + val cachedLibrary = findCachedLibrary(libraryName, version) + cachedLibrary.map(Success(_)).getOrElse { logger.trace( s"$libraryName was not found in any caches, it will need to be " + s"downloaded." diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/PublishedLibraryCache.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/PublishedLibraryCache.scala index 50d661a447ee..0ee813b09704 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/PublishedLibraryCache.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/PublishedLibraryCache.scala @@ -4,6 +4,7 @@ import nl.gn0s1s.bump.SemVer import org.enso.editions.LibraryName import org.enso.librarymanager.LibraryLocations import org.enso.librarymanager.published.bundles.LocalReadOnlyRepository +import org.enso.librarymanager.resolved.LibraryRoot import java.nio.file.Path @@ -18,7 +19,10 @@ trait PublishedLibraryCache { def isLibraryCached(libraryName: LibraryName, version: SemVer): Boolean /** 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 { diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/PublishedLibraryProvider.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/PublishedLibraryProvider.scala index c224116f111e..4cbb1341e92c 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/PublishedLibraryProvider.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/PublishedLibraryProvider.scala @@ -3,8 +3,8 @@ package org.enso.librarymanager.published import nl.gn0s1s.bump.SemVer import org.enso.editions.Editions.Repository import org.enso.editions.LibraryName +import org.enso.librarymanager.resolved.LibraryRoot -import java.nio.file.Path import scala.util.Try /** A provider of published libraries. @@ -24,5 +24,5 @@ trait PublishedLibraryProvider { libraryName: LibraryName, version: SemVer, recommendedRepository: Repository - ): Try[Path] + ): Try[LibraryRoot] } diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/bundles/LocalReadOnlyRepository.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/bundles/LocalReadOnlyRepository.scala index d225351b6f07..05333a5aa06c 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/bundles/LocalReadOnlyRepository.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/bundles/LocalReadOnlyRepository.scala @@ -7,6 +7,7 @@ import org.enso.librarymanager.published.cache.{ LibraryCache, ReadOnlyLibraryCache } +import org.enso.librarymanager.resolved.LibraryRoot import org.enso.logger.masking.MaskedPath import java.nio.file.{Files, Path} @@ -31,13 +32,13 @@ class LocalReadOnlyRepository(root: Path) extends ReadOnlyLibraryCache { override def findCachedLibrary( libraryName: LibraryName, version: SemVer - ): Option[Path] = { + ): Option[LibraryRoot] = { val path = LibraryCache.resolvePath(root, libraryName, version) if (Files.exists(path)) { logger.trace( s"$libraryName found at [${MaskedPath(path).applyMasking()}]." ) - Some(path) + Some(LibraryRoot(path)) } else { logger.trace( s"Did not find $libraryName at [${MaskedPath(path).applyMasking()}]." diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/cache/DownloadingLibraryCache.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/cache/DownloadingLibraryCache.scala index 930b63e8e92f..e34fbe793dff 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/cache/DownloadingLibraryCache.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/cache/DownloadingLibraryCache.scala @@ -18,11 +18,13 @@ import org.enso.librarymanager.published.repository.RepositoryHelper.{ LibraryAccess, RepositoryMethods } +import org.enso.librarymanager.resolved.LibraryRoot import org.enso.logger.masking.MaskedPath import org.enso.pkg.PackageManager import org.enso.yaml.YamlHelper import java.nio.file.{Files, Path} + import scala.util.control.NonFatal import scala.util.{Success, Try} @@ -50,7 +52,7 @@ class DownloadingLibraryCache( override def findCachedLibrary( libraryName: LibraryName, version: SemVer - ): Option[Path] = { + ): Option[LibraryRoot] = { val path = LibraryCache.resolvePath(cacheRoot, libraryName, version) resourceManager.withResource( lockUserInterface, @@ -62,7 +64,7 @@ class DownloadingLibraryCache( s"Library [$libraryName:$version] found cached at " + s"[${MaskedPath(path).applyMasking()}]." ) - Some(path) + Some(LibraryRoot(path)) } else None } } @@ -72,7 +74,7 @@ class DownloadingLibraryCache( libraryName: LibraryName, version: SemVer, recommendedRepository: Editions.Repository - ): Try[Path] = { + ): Try[LibraryRoot] = { val cached = findCachedLibrary(libraryName, version) cached match { case Some(result) => Success(result) @@ -95,7 +97,7 @@ class DownloadingLibraryCache( libraryName: LibraryName, version: SemVer, recommendedRepository: Editions.Repository - ): Try[Path] = Try { + ): Try[LibraryRoot] = Try { logger.trace(s"Trying to install [$libraryName:$version].") resourceManager.withResource( lockUserInterface, @@ -108,7 +110,7 @@ class DownloadingLibraryCache( logger.info( s"Another process has just installed [$libraryName:$version]." ) - cachedLibraryPath + LibraryRoot(cachedLibraryPath) } else { val access = recommendedRepository.accessLibrary(libraryName, version) val manifest = downloadManifest(libraryName, access) @@ -129,7 +131,7 @@ class DownloadingLibraryCache( destination = cachedLibraryPath ) - cachedLibraryPath + LibraryRoot(cachedLibraryPath) } catch { case NonFatal(exception) => logger.error( @@ -149,7 +151,7 @@ class DownloadingLibraryCache( libraryName: LibraryName, access: LibraryAccess ): LibraryManifest = { - val manifestDownload = access.downloadManifest() + val manifestDownload = access.fetchManifest() progressReporter.trackProgress( s"Downloading library manifest of [$libraryName].", manifestDownload diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/cache/LibraryCache.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/cache/LibraryCache.scala index ef28655277f8..f596b45ebf37 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/cache/LibraryCache.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/cache/LibraryCache.scala @@ -2,8 +2,10 @@ package org.enso.librarymanager.published.cache import nl.gn0s1s.bump.SemVer import org.enso.editions.{Editions, LibraryName, LibraryVersion} +import org.enso.librarymanager.resolved.LibraryRoot import java.nio.file.Path + import scala.util.Try /** A library cache that is also capable of downloading missing libraries (which @@ -26,7 +28,7 @@ trait LibraryCache extends ReadOnlyLibraryCache { override def findCachedLibrary( libraryName: LibraryName, version: SemVer - ): Option[Path] + ): Option[LibraryRoot] /** If the cache contains the library, it is returned immediately, otherwise, * it tries to download the missing library. @@ -46,7 +48,7 @@ trait LibraryCache extends ReadOnlyLibraryCache { libraryName: LibraryName, version: SemVer, recommendedRepository: Editions.Repository - ): Try[Path] + ): Try[LibraryRoot] /** Ensures that the given library and all of its dependencies are installed. * diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/cache/ReadOnlyLibraryCache.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/cache/ReadOnlyLibraryCache.scala index 069218501839..459e76a68f85 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/cache/ReadOnlyLibraryCache.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/cache/ReadOnlyLibraryCache.scala @@ -2,8 +2,7 @@ package org.enso.librarymanager.published.cache import nl.gn0s1s.bump.SemVer import org.enso.editions.LibraryName - -import java.nio.file.Path +import org.enso.librarymanager.resolved.LibraryRoot /** A read-only cache may contain some pre-defined set of libraries, but it does * 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 * has been found. */ - def findCachedLibrary(libraryName: LibraryName, version: SemVer): Option[Path] + def findCachedLibrary( + libraryName: LibraryName, + version: SemVer + ): Option[LibraryRoot] } diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/repository/RepositoryHelper.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/repository/RepositoryHelper.scala index 146656f35f96..6aeef6dd03a8 100644 --- a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/repository/RepositoryHelper.scala +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/published/repository/RepositoryHelper.scala @@ -6,10 +6,10 @@ import org.enso.distribution.FileSystem.PathSyntax import org.enso.downloader.http.{HTTPDownload, URIBuilder} import org.enso.editions.Editions.Repository import org.enso.editions.LibraryName -import org.enso.pkg.Package +import org.enso.pkg.{Config, Package} import org.enso.yaml.YamlHelper - import java.nio.file.Path + import scala.util.Failure /** A class that manages the HTTP API of the Library Repository. @@ -52,15 +52,15 @@ object RepositoryHelper { 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 * [[LibraryNotFoundException]] indicating that the repository does not * provide that library. Any other failures are indicated with the more * generic [[LibraryDownloadFailure]]. */ - def downloadManifest(): TaskProgress[LibraryManifest] = { - val url = (libraryRoot / manifestFilename).build() + def fetchManifest(): TaskProgress[LibraryManifest] = { + val url = (libraryRoot / LibraryManifest.filename).build() HTTPDownload.fetchString(url).flatMap { response => response.statusCode match { 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. */ private def downloadArtifact( artifactName: String, @@ -108,9 +136,6 @@ object RepositoryHelper { ): TaskProgress[Unit] = downloadArtifact(archiveName, destinationDirectory) } - /** Name of the manifest file. */ - val manifestFilename = "manifest.yaml" - /** Name of the attached license file. */ val licenseFilename = "LICENSE.md" diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/resolved/FilesystemLibraryReadAccess.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/resolved/FilesystemLibraryReadAccess.scala new file mode 100644 index 000000000000..5e68f92addca --- /dev/null +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/resolved/FilesystemLibraryReadAccess.scala @@ -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) + } + +} diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/resolved/LibraryReadAccess.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/resolved/LibraryReadAccess.scala new file mode 100644 index 000000000000..3c79e9fe06fa --- /dev/null +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/resolved/LibraryReadAccess.scala @@ -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] +} diff --git a/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/resolved/LibraryRoot.scala b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/resolved/LibraryRoot.scala new file mode 100644 index 000000000000..d73a08d1f04e --- /dev/null +++ b/lib/scala/library-manager/src/main/scala/org/enso/librarymanager/resolved/LibraryRoot.scala @@ -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) + } +} diff --git a/lib/scala/library-manager/src/test/scala/org/enso/librarymanager/LibraryResolverSpec.scala b/lib/scala/library-manager/src/test/scala/org/enso/librarymanager/LibraryResolverSpec.scala index 567f1711de1d..ce5d66e29dff 100644 --- a/lib/scala/library-manager/src/test/scala/org/enso/librarymanager/LibraryResolverSpec.scala +++ b/lib/scala/library-manager/src/test/scala/org/enso/librarymanager/LibraryResolverSpec.scala @@ -4,6 +4,7 @@ import nl.gn0s1s.bump.SemVer import org.enso.editions.Editions.Repository import org.enso.editions.{Editions, LibraryName, LibraryVersion} import org.enso.librarymanager.local.LocalLibraryProvider +import org.enso.librarymanager.resolved.LibraryRoot import org.enso.testkit.EitherValue import org.scalatest.Inside import org.scalatest.matchers.should.Matchers @@ -57,8 +58,14 @@ class LibraryResolverSpec case class FakeLocalLibraryProvider(fixtures: Map[LibraryName, Path]) 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( diff --git a/lib/scala/pkg/src/main/scala/org/enso/pkg/ComponentGroup.scala b/lib/scala/pkg/src/main/scala/org/enso/pkg/ComponentGroup.scala index eb3444a9bd2a..8331f659871c 100644 --- a/lib/scala/pkg/src/main/scala/org/enso/pkg/ComponentGroup.scala +++ b/lib/scala/pkg/src/main/scala/org/enso/pkg/ComponentGroup.scala @@ -102,14 +102,10 @@ object ComponentGroup { /** The definition of a component group that extends an existing one. * * @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 */ case class ExtendedComponentGroup( module: ModuleReference, - color: Option[String], - icon: Option[String], exports: Seq[Component] ) object ExtendedComponentGroup { @@ -117,22 +113,17 @@ object ExtendedComponentGroup { /** Fields for use when serializing the [[ExtendedComponentGroup]]. */ private object Fields { val Module = "module" - val Color = "color" - val Icon = "icon" val Exports = "exports" } /** [[Encoder]] instance for the [[ExtendedComponentGroup]]. */ implicit val encoder: Encoder[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)( Fields.Exports -> extendedComponentGroup.exports.asJson ) Json.obj( - (Fields.Module -> extendedComponentGroup.module.asJson) +: - (color.toSeq ++ icon.toSeq ++ exports.toSeq): _* + (Fields.Module -> extendedComponentGroup.module.asJson) +: exports.toSeq: _* ) } @@ -145,10 +136,8 @@ object ExtendedComponentGroup { Fields.Module, json ) - color <- json.get[Option[String]](Fields.Color) - icon <- json.get[Option[String]](Fields.Icon) exports <- json.getOrElse[List[Component]](Fields.Exports)(List()) - } yield ExtendedComponentGroup(reference, color, icon, exports) + } yield ExtendedComponentGroup(reference, exports) } } @@ -219,7 +208,18 @@ object Shortcut { case class ModuleReference( libraryName: LibraryName, 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 { private def toModuleString(moduleReference: ModuleReference): String = { diff --git a/lib/scala/pkg/src/test/scala/org/enso/pkg/ConfigSpec.scala b/lib/scala/pkg/src/test/scala/org/enso/pkg/ConfigSpec.scala index fbaae5d0d756..d9aeece8937d 100644 --- a/lib/scala/pkg/src/test/scala/org/enso/pkg/ConfigSpec.scala +++ b/lib/scala/pkg/src/test/scala/org/enso/pkg/ConfigSpec.scala @@ -134,8 +134,6 @@ class ConfigSpec LibraryName("Standard", "Base"), ModuleName("Group 2") ), - color = None, - icon = None, exports = List(Component("bax", None)) ) )