mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 10:11:37 +03:00
Add API for component groups (#3286)
This commit is contained in:
parent
2ae636f63c
commit
3858ae7517
@ -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
|
||||
|
||||
|
@ -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>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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 }"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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.
|
||||
*/
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
@ -107,7 +107,7 @@ class LibraryPublishHandler(
|
||||
new CompilerBasedDependencyExtractor(logLevel)
|
||||
LibraryUploader(dependencyExtractor)
|
||||
.uploadLibrary(
|
||||
libraryRoot,
|
||||
libraryRoot.location,
|
||||
uploadUrl,
|
||||
token,
|
||||
progressReporter
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -62,8 +62,6 @@ class RuntimeComponentsTest
|
||||
LibraryName("Standard", "Base"),
|
||||
ModuleName("Group2")
|
||||
),
|
||||
color = None,
|
||||
icon = None,
|
||||
exports = List(Component("foo", None))
|
||||
)
|
||||
)
|
||||
|
@ -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."
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -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."
|
||||
|
@ -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 {
|
||||
|
@ -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]
|
||||
}
|
||||
|
@ -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()}]."
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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]
|
||||
}
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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]
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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 = {
|
||||
|
@ -134,8 +134,6 @@ class ConfigSpec
|
||||
LibraryName("Standard", "Base"),
|
||||
ModuleName("Group 2")
|
||||
),
|
||||
color = None,
|
||||
icon = None,
|
||||
exports = List(Component("bax", None))
|
||||
)
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user