Add API for component groups (#3286)

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

View File

@ -12,6 +12,9 @@
- Updated to
[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

View File

@ -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)
<!-- /MarkdownTOC -->
@ -1418,6 +1427,114 @@ interface NamedEdition {
}
```
### `ComponentGroups`
The description of component groups provided by the package. Object fields can
be omitted if the corresponding list is empty.
```typescript
interface ComponentGroups {
/** The list of component groups provided by the package. */
newGroups?: ComponentGroup[];
/** The list of component groups that this package extends.*/
extendedGroups?: ExtendedComponentGroup[];
}
```
### `ComponentGroup`
The definition of a single component group.
```typescript
interface ComponentGroup {
/** The module name containing the declared componennts. */
module: string;
color?: string;
icon?: string;
/** The list of components provided by this component group. */
exports: Component[];
}
```
### `ExtendedComponentGroup`
The definition of a component group that extends an existing one.
```typescript
interface ExtendedComponentGroup {
/** The reference to the component group module being extended. */
module: ModuleReference;
/** The list of components provided by this component group. */
exports: Component[];
}
```
### `ModuleReference`
The reference to a module.
```typescript
interface ModuleReference {
/**
* A string consisting of a namespace and a lirary name separated by the dot
* <namespace>.<library name>, i.e. `Standard.Base`.
*/
libraryName: string;
/** The module name without the library name prefix.
* E.g. given the `Standard.Base.Data.Vector` module reference,
* the `moduleName` field contains `Data.Vector`.
*/
moduleName: string;
}
```
### `Component`
A single component of a component group.
```typescript
interface Component {
/** The component name. */
name: string;
/** The component shortcut. */
shortcut?: string;
}
```
### `LibraryComponentGroup`
The component group provided by a library.
```typescript
interface LibraryComponentGroup {
/**
* A string consisting of a namespace and a lirary name separated by the dot
* <namespace>.<library name>, i.e. `Standard.Base`.
*/
library: string;
/** The module name without the library name prefix.
* E.g. given the `Standard.Base.Data.Vector` module reference,
* the `module` field contains `Data.Vector`.
*/
module: string;
color?: string;
icon?: string;
/** The list of components provided by this component group. */
exports: Component[];
}
```
## Connection Management
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: <reason>."
}
```
### `InvalidSemverVersion`
Signals that the provided version string is not a valid semver version. The
message contains the invalid version in the payload.
```typescript
"error" : {
"code" : 8011,
"message" : "[<invalid-version>] is not a valid semver version.",
"payload" : {
"version" : "<invalid-version>"
}
}
```

View File

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

View File

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

View File

@ -1,10 +1,10 @@
package org.enso.languageserver.libraries
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 }"""
)
}
}

View File

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

View File

@ -2,7 +2,6 @@ package org.enso.languageserver.libraries
import akka.actor.Props
import 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.
*/

View File

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

View File

@ -1,9 +1,9 @@
package org.enso.languageserver.libraries
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
}

View File

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

View File

@ -1,6 +1,8 @@
package org.enso.languageserver.libraries.handler
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
)
)
}

View File

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

View File

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

View File

@ -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
)
)
}

View File

@ -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)

View File

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

View File

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

View File

@ -13,7 +13,19 @@ import org.enso.librarymanager.published.repository.{
ExampleRepository,
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

View File

@ -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

View File

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

View File

@ -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."
)

View File

@ -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

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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.
*/

View File

@ -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 {

View File

@ -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(

View File

@ -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."

View File

@ -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 {

View File

@ -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]
}

View File

@ -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()}]."

View File

@ -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

View File

@ -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.
*

View File

@ -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]
}

View File

@ -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"

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import nl.gn0s1s.bump.SemVer
import org.enso.editions.Editions.Repository
import org.enso.editions.{Editions, 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(

View File

@ -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 = {

View File

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