Add support for the component groups syntax (#3235)

This commit is contained in:
Dmitry Bushev 2022-01-26 18:57:50 +03:00 committed by GitHub
parent 3905698b41
commit e6d9b5741d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 635 additions and 29 deletions

View File

@ -14,13 +14,13 @@ import org.enso.pkg.{Contact, PackageManager, Template}
import org.enso.polyglot.{LanguageInfo, Module, PolyglotContext}
import org.enso.version.VersionDescription
import org.graalvm.polyglot.PolyglotException
import java.io.File
import java.nio.file.Path
import java.util.UUID
import scala.Console.err
import scala.jdk.CollectionConverters._
import scala.util.Try
import scala.util.{Failure, Success, Try}
import scala.util.control.NonFatal
/** The main CLI entry point class. */
@ -509,16 +509,20 @@ object Main {
enableAutoParallelism = enableAutoParallelism
)
if (projectMode) {
val pkg = PackageManager.Default.fromDirectory(file)
val main = pkg.map(_.mainFile)
if (!main.exists(_.exists())) {
PackageManager.Default.loadPackage(file) match {
case Success(pkg) =>
val main = pkg.mainFile
if (!main.exists()) {
println("Main file does not exist.")
context.context.close()
exitFail()
}
val mainFile = main.get
val mainModuleName = pkg.get.moduleNameForFile(mainFile).toString
val mainModuleName = pkg.moduleNameForFile(pkg.mainFile).toString
runPackage(context, mainModuleName, file)
case Failure(ex) =>
println(ex.getMessage)
exitFail()
}
} else {
runSingleFile(context, file)
}

View File

@ -14,7 +14,7 @@ case class LibraryName(namespace: String, name: String) {
/** The qualified name of the library consists of its prefix and name
* separated with a dot.
*/
def qualifiedName: String = s"$namespace.$name"
def qualifiedName: String = s"$namespace${LibraryName.separator}$name"
/** @inheritdoc */
override def toString: String = qualifiedName
@ -36,7 +36,7 @@ object LibraryName {
libraryName.toString.asJson
}
private val separator = '.'
val separator = '.'
/** Creates a [[LibraryName]] from its string representation.
*

View File

@ -0,0 +1,293 @@
package org.enso.pkg
import io.circe._
import io.circe.syntax._
import org.enso.editions.LibraryName
/** The description of component groups provided by the package.
*
* @param newGroups the list of component groups provided by the package
* @param extendedGroups the list of component groups that this package extends
*/
case class ComponentGroups(
newGroups: List[ComponentGroup],
extendedGroups: List[ExtendedComponentGroup]
)
object ComponentGroups {
/** Empty component groups. */
val empty: ComponentGroups =
ComponentGroups(List(), List())
/** Fields for use when serializing the [[ComponentGroups]]. */
private object Fields {
val New = "new"
val Extends = "extends"
}
/** [[Encoder]] instance for the [[ComponentGroups]]. */
implicit val encoder: Encoder[ComponentGroups] = { componentGroups =>
val newGroups = Option.unless(componentGroups.newGroups.isEmpty)(
Fields.New -> componentGroups.newGroups.asJson
)
val extendsGroups = Option.unless(componentGroups.extendedGroups.isEmpty)(
Fields.Extends -> componentGroups.extendedGroups.asJson
)
Json.obj(newGroups.toSeq ++ extendsGroups.toSeq: _*)
}
/** [[Decoder]] instance for the [[ComponentGroups]]. */
implicit val decoder: Decoder[ComponentGroups] = { json =>
for {
newGroups <- json.getOrElse[List[ComponentGroup]](Fields.New)(List())
extendsGroups <-
json.getOrElse[List[ExtendedComponentGroup]](Fields.Extends)(List())
} yield ComponentGroups(newGroups, extendsGroups)
}
}
/** The definition of a single component group.
*
* @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 ComponentGroup(
module: ModuleName,
color: Option[String],
icon: Option[String],
exports: Seq[Component]
)
object ComponentGroup {
/** Fields for use when serializing the [[ComponentGroup]]. */
private object Fields {
val Module = "module"
val Color = "color"
val Icon = "icon"
val Exports = "exports"
}
/** [[Encoder]] instance for the [[ComponentGroup]]. */
implicit val encoder: Encoder[ComponentGroup] = { 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.Module -> componentGroup.module.asJson) +:
(color.toSeq ++ icon.toSeq ++ exports.toSeq): _*
)
}
/** [[Decoder]] instance for the [[ComponentGroup]]. */
implicit val decoder: Decoder[ComponentGroup] = { json =>
for {
name <- ConfigCodecs
.getFromObject[ModuleName](
"component group name",
Fields.Module,
json
)
color <- json.get[Option[String]](Fields.Color)
icon <- json.get[Option[String]](Fields.Icon)
exports <- json.getOrElse[List[Component]](Fields.Exports)(List())
} yield ComponentGroup(name, color, icon, exports)
}
}
/** 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 {
/** 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): _*
)
}
/** [[Decoder]] instance for the [[ExtendedComponentGroup]]. */
implicit val decoder: Decoder[ExtendedComponentGroup] = { json =>
for {
reference <- ConfigCodecs
.getFromObject[ModuleReference](
"extended component group reference",
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)
}
}
/** A single component of a component group.
*
* @param name the component name
* @param shortcut the component shortcut
*/
case class Component(name: String, shortcut: Option[Shortcut])
object Component {
object Fields {
val Name = "name"
val Shortcut = "shortcut"
}
/** [[Encoder]] instance for the [[Component]]. */
implicit val encoder: Encoder[Component] = { component =>
component.shortcut match {
case Some(shortcut) =>
Json.obj(
Fields.Name -> component.name.asJson,
Fields.Shortcut -> shortcut.asJson
)
case None =>
component.name.asJson
}
}
/** [[Decoder]] instance for the [[Component]]. */
implicit val decoder: Decoder[Component] = { json =>
json.as[String] match {
case Right(name) =>
Right(Component(name, None))
case Left(_) =>
for {
name <- ConfigCodecs
.getFromObject[String]("component name", Fields.Name, json)
shortcut <- json.getOrElse[Option[Shortcut]](Fields.Shortcut)(None)
} yield Component(name, shortcut)
}
}
}
/** The shortcut reference to the component.
*
* @param key the shortcut key combination
*/
case class Shortcut(key: String)
object Shortcut {
/** [[Encoder]] instance for the [[Shortcut]]. */
implicit val encoder: Encoder[Shortcut] = { shortcut =>
shortcut.key.asJson
}
/** [[Decoder]] instance for the [[Shortcut]]. */
implicit val decoder: Decoder[Shortcut] = { json =>
ConfigCodecs.getScalar("shortcut", json).map(Shortcut(_))
}
}
/** The reference to a module.
*
* @param libraryName the qualified name of a library where the module is defined
* @param moduleName the module name
*/
case class ModuleReference(
libraryName: LibraryName,
moduleName: Option[ModuleName]
)
object ModuleReference {
private def toModuleString(moduleReference: ModuleReference): String = {
val libraryName =
s"${moduleReference.libraryName.namespace}${LibraryName.separator}${moduleReference.libraryName.name}"
moduleReference.moduleName.fold(libraryName) { moduleName =>
s"$libraryName${LibraryName.separator}${moduleName.name}"
}
}
/** [[Encoder]] instance for the [[ModuleReference]]. */
implicit val encoder: Encoder[ModuleReference] = { moduleReference =>
toModuleString(moduleReference).asJson
}
/** [[Decoder]] instance for the [[ModuleReference]]. */
implicit val decoder: Decoder[ModuleReference] = { json =>
json.as[String].flatMap { moduleString =>
moduleString.split(LibraryName.separator).toList match {
case namespace :: name :: module =>
Right(
ModuleReference(
LibraryName(namespace, name),
ModuleName.fromComponents(module)
)
)
case _ =>
Left(
DecodingFailure(
s"Failed to decode '$moduleString' as module reference. " +
s"Module reference should consist of a namespace (author), " +
s"library name and a module name (e.g. Standard.Base.Data).",
json.history
)
)
}
}
}
}
/** The module name.
*
* @param name the module name
*/
case class ModuleName(name: String)
object ModuleName {
def fromComponents(items: List[String]): Option[ModuleName] =
Option.unless(items.isEmpty)(ModuleName(items.mkString(".")))
/** [[Encoder]] instance for the [[ModuleName]]. */
implicit val encoder: Encoder[ModuleName] = { moduleName =>
moduleName.name.asJson
}
/** [[Decoder]] instance for the [[ModuleName]]. */
implicit val decoder: Decoder[ModuleName] = { json =>
json.as[String] match {
case Left(_) =>
Left(
DecodingFailure(
"Failed to decode component group name.",
json.history
)
)
case Right(name) =>
Right(ModuleName(name))
}
}
}

View File

@ -99,6 +99,8 @@ object Contact {
* @param preferLocalLibraries specifies if library resolution should prefer
* local libraries over what is defined in the
* edition
* @param componentGroups the description of component groups provided by this
* package
* @param originalJson a Json object holding the original values that this
* Config was created from, used to preserve configuration
* keys that are not known
@ -112,6 +114,7 @@ case class Config(
maintainers: List[Contact],
edition: Option[Editions.RawEdition],
preferLocalLibraries: Boolean,
componentGroups: Either[DecodingFailure, ComponentGroups],
originalJson: JsonObject = JsonObject()
) {
@ -134,6 +137,7 @@ object Config {
val maintainer: String = "maintainers"
val edition: String = "edition"
val preferLocalLibraries = "prefer-local-libraries"
val componentGroups = "component-groups"
}
implicit val decoder: Decoder[Config] = { json =>
@ -166,7 +170,13 @@ object Config {
error => DecodingFailure(error, json.history)
}
originals <- json.as[JsonObject]
} yield Config(
} yield {
val componentGroups =
json.getOrElse[ComponentGroups](JsonFields.componentGroups)(
ComponentGroups.empty
)
Config(
name = name,
namespace = namespace,
version = version,
@ -175,9 +185,11 @@ object Config {
maintainers = maintainer,
edition = finalEdition,
preferLocalLibraries = preferLocal,
componentGroups = componentGroups,
originalJson = originals
)
}
}
implicit val encoder: Encoder[Config] = { config =>
val originals = config.originalJson
@ -189,6 +201,12 @@ object Config {
}
.map(JsonFields.edition -> _)
val componentGroups = config.componentGroups.toOption.flatMap { value =>
Option.unless(value.newGroups.isEmpty && value.extendedGroups.isEmpty)(
JsonFields.componentGroups -> value.asJson
)
}
val overrides = Seq(
JsonFields.name -> config.name.asJson,
JsonFields.namespace -> config.namespace.asJson,
@ -196,7 +214,7 @@ object Config {
JsonFields.license -> config.license.asJson,
JsonFields.author -> config.authors.asJson,
JsonFields.maintainer -> config.maintainers.asJson
) ++ edition.toSeq
) ++ edition.toSeq ++ componentGroups.toSeq
val preferLocalOverride =
if (config.preferLocalLibraries)

View File

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

View File

@ -224,7 +224,8 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) {
edition: Option[Editions.RawEdition] = None,
authors: List[Contact] = List(),
maintainers: List[Contact] = List(),
license: String = ""
license: String = "",
componentGroups: ComponentGroups = ComponentGroups.empty
): Package[F] = {
val config = Config(
name = NameValidation.normalizeName(name),
@ -234,7 +235,8 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) {
authors = authors,
edition = edition,
preferLocalLibraries = true,
maintainers = maintainers
maintainers = maintainers,
componentGroups = Right(componentGroups)
)
create(root, config, template)
}

View File

@ -1,7 +1,9 @@
package org.enso.pkg
import io.circe.{Json, JsonObject}
import cats.Show
import io.circe.{DecodingFailure, Json, JsonObject}
import nl.gn0s1s.bump.SemVer
import org.enso.editions.LibraryName
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.{Inside, OptionValues}
@ -11,6 +13,7 @@ class ConfigSpec
with Matchers
with Inside
with OptionValues {
"Config" should {
"preserve unknown keys when deserialized and serialized again" in {
val original = Json.obj(
@ -42,7 +45,8 @@ class ConfigSpec
Contact(Some("B"), None),
Contact(None, Some("c@example.com"))
),
preferLocalLibraries = true
preferLocalLibraries = true,
componentGroups = Right(ComponentGroups.empty)
)
val deserialized = Config.fromYaml(config.toYaml).get
val withoutJson = deserialized.copy(originalJson = JsonObject())
@ -90,4 +94,215 @@ class ConfigSpec
serialized should include("edition: '2020.1'")
}
}
"Component groups" should {
"correctly de-serialize and serialize back the components syntax" in {
val config =
"""name: FooBar
|component-groups:
| new:
| - Group 1:
| color: '#C047AB'
| icon: icon-name
| exports:
| - foo:
| shortcut: f
| - bar
| extends:
| - Standard.Base.Group 2:
| exports:
| - bax
|""".stripMargin
val parsed = Config.fromYaml(config).get
val expectedComponentGroups = ComponentGroups(
newGroups = List(
ComponentGroup(
module = ModuleName("Group 1"),
color = Some("#C047AB"),
icon = Some("icon-name"),
exports = List(
Component("foo", Some(Shortcut("f"))),
Component("bar", None)
)
)
),
extendedGroups = List(
ExtendedComponentGroup(
module = ModuleReference(
LibraryName("Standard", "Base"),
Some(ModuleName("Group 2"))
),
color = None,
icon = None,
exports = List(Component("bax", None))
)
)
)
parsed.componentGroups shouldEqual Right(expectedComponentGroups)
val serialized = parsed.toYaml
serialized should include(
"""component-groups:
| new:
| - module: Group 1
| color: '#C047AB'
| icon: icon-name
| exports:
| - name: foo
| shortcut: f
| - bar
| extends:
| - module: Standard.Base.Group 2
| exports:
| - bax""".stripMargin.linesIterator.mkString("\n")
)
}
"correctly de-serialize empty components" in {
val config =
"""name: FooBar
|component-groups:
|""".stripMargin
val parsed = Config.fromYaml(config).get
parsed.componentGroups shouldEqual Right(ComponentGroups.empty)
}
"allow unknown keys in component groups" in {
val config =
"""name: FooBar
|component-groups:
| foo:
| - Group 1:
| exports:
| - bax
|""".stripMargin
val parsed = Config.fromYaml(config).get
parsed.componentGroups shouldEqual Right(ComponentGroups.empty)
}
"fail to de-serialize invalid extended modules" in {
val config =
"""name: FooBar
|component-groups:
| extends:
| - Group 1:
| exports:
| - bax
|""".stripMargin
val parsed = Config.fromYaml(config).get
parsed.componentGroups match {
case Left(f: DecodingFailure) =>
Show[DecodingFailure].show(f) should include(
"Failed to decode 'Group 1' as module reference"
)
case unexpected =>
fail(s"Unexpected result: $unexpected")
}
}
"correctly de-serialize shortcuts" in {
val config =
"""name: FooBar
|component-groups:
| new:
| - Group 1:
| exports:
| - foo:
| shortcut: f
| - bar:
| shortcut: fgh
| - baz:
| shortcut: 0
| - quux:
| shortcut:
| - hmmm:
|""".stripMargin
val parsed = Config.fromYaml(config).get
val expectedComponentGroups = ComponentGroups(
newGroups = List(
ComponentGroup(
module = ModuleName("Group 1"),
color = None,
icon = None,
exports = List(
Component("foo", Some(Shortcut("f"))),
Component("bar", Some(Shortcut("fgh"))),
Component("baz", Some(Shortcut("0"))),
Component("quux", None),
Component("hmmm", None)
)
)
),
extendedGroups = List()
)
parsed.componentGroups shouldEqual Right(expectedComponentGroups)
}
"fail to de-serialize invalid shortcuts" in {
val config =
"""name: FooBar
|component-groups:
| new:
| - Group 1:
| exports:
| - foo:
| shortcut: []
|""".stripMargin
val parsed = Config.fromYaml(config).get
parsed.componentGroups match {
case Left(f: DecodingFailure) =>
Show[DecodingFailure].show(f) should include(
"Failed to decode shortcut"
)
case unexpected =>
fail(s"Unexpected result: $unexpected")
}
}
"fail to de-serialize invalid component groups" in {
val config =
"""name: FooBar
|component-groups:
| new:
| - exports:
| - name: foo
|""".stripMargin
val parsed = Config.fromYaml(config).get
parsed.componentGroups match {
case Left(f: DecodingFailure) =>
Show[DecodingFailure].show(f) should include(
"Failed to decode component group name"
)
case unexpected =>
fail(s"Unexpected result: $unexpected")
}
}
"fail to de-serialize invalid components" in {
val config =
"""name: FooBar
|component-groups:
| new:
| - Group 1:
| exports:
| - one: two
|""".stripMargin
val parsed = Config.fromYaml(config).get
parsed.componentGroups match {
case Left(f: DecodingFailure) =>
Show[DecodingFailure].show(f) should include(
"Failed to decode component name"
)
case unexpected =>
fail(s"Unexpected result: $unexpected")
}
}
}
}