Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional scripts section in libraries' config #11555

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/distribution/packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,24 @@ If the flag is not specified, it defaults to `false`, delegating all library
resolution to the edition configuration. However, newly created projects will
have it set to `true`.

### scripts

**Optional** _List of scripts_: The name(s) of scripts that are supported by the
given library and should be invoked when an action is requested. Scripts
themselves can take a list of arguments and are constrained by the format of the
config file.

Currently supported scripts are:

- **Refresh** A list of Fully Qualified Names of methods of the library to be
called when a project is being recomputed. For example

```yaml
refresh:
- Standard.Base.Http.Caches.refresh
- Standard.Base.Caches.refresh
```

### The `visualization` Directory

As Enso is a visual language, a package may contain a specification of how data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@ object ComponentGroupsResolverSpec {
maintainers = Nil,
edition = None,
preferLocalLibraries = true,
componentGroups = Some(componentGroups)
componentGroups = Some(componentGroups),
scripts = Nil
)

/** Create a new config. */
Expand All @@ -298,7 +299,8 @@ object ComponentGroupsResolverSpec {
maintainers = Nil,
edition = None,
preferLocalLibraries = true,
componentGroups = None
componentGroups = None,
scripts = Nil
)

/** Create a new component group. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,8 @@ object ComponentGroupsValidatorSpec {
),
extendedGroups = Nil
)
)
),
scripts = Nil
)

/** Create a library name from config. */
Expand Down
4 changes: 3 additions & 1 deletion engine/runner/src/main/java/org/enso/runner/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
import scala.Option$;
import scala.collection.immutable.List$;
import scala.concurrent.ExecutionContext;
import scala.concurrent.ExecutionContextExecutor;
import scala.concurrent.duration.FiniteDuration;
Expand Down Expand Up @@ -599,7 +600,8 @@ private void createNew(
authors,
nil(),
"",
Option$.MODULE$.empty());
Option$.MODULE$.empty(),
List$.MODULE$.empty());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can use nil() helper method (see two lines above)

throw exitSuccess();
}

Expand Down
41 changes: 36 additions & 5 deletions lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,7 @@ object Contact {
* 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
* @param scripts list of scripts that could be called for the given library
*/
case class Config(
name: String,
Expand All @@ -109,7 +107,8 @@ case class Config(
maintainers: List[Contact],
edition: Option[Editions.RawEdition],
preferLocalLibraries: Boolean,
componentGroups: Option[ComponentGroups]
componentGroups: Option[ComponentGroups],
scripts: List[Script]
) {

/** Converts the configuration into a YAML representation. */
Expand Down Expand Up @@ -147,6 +146,7 @@ object Config {
val Edition: String = "edition"
val PreferLocalLibraries = "prefer-local-libraries"
val ComponentGroups = "component-groups"
val Scripts: String = "scripts"
}

implicit val yamlDecoder: YamlDecoder[Config] =
Expand All @@ -164,6 +164,7 @@ object Config {
val booleanDecoder = implicitly[YamlDecoder[Boolean]]
val componentGroups =
implicitly[YamlDecoder[Option[ComponentGroups]]]
val scriptsDecoder = implicitly[YamlDecoder[List[Script]]]
for {
name <- clazzMap
.get(JsonFields.Name)
Expand Down Expand Up @@ -220,6 +221,11 @@ object Config {
.get(JsonFields.ComponentGroups)
.map(componentGroups.decode)
.getOrElse(Right(None))
scripts <- clazzMap
.get(JsonFields.Scripts)
.map(scriptsDecoder.decode)
.getOrElse(Right(Nil))
.flatMap(validateScripts)
} yield Config(
name,
normalizedName,
Expand All @@ -230,9 +236,27 @@ object Config {
maintainers,
edition,
preferLocalLibraries,
componentGroups
componentGroups,
scripts
)
}

private def validateScripts(
scripts: List[Script]
): Either[Throwable, List[Script]] = {
val grouped = scripts.groupBy(_.name)
val nonUnique = grouped.find(_._2.length > 1)
nonUnique match {
case Some((scriptName, groupedScripts)) =>
Left(
new YAMLException(
s"Scripts have to be unique, got ${groupedScripts.length} for $scriptName"
)
)
case _ =>
Right(scripts)
}
}
}

implicit val encoderSnake: YamlEncoder[Config] =
Expand All @@ -243,6 +267,7 @@ object Config {
val booleanEncoder = implicitly[YamlEncoder[Boolean]]
val componentGroupsEncoder =
implicitly[YamlEncoder[ComponentGroups]]
val scriptsEncoder = implicitly[YamlEncoder[List[Script]]]

val elements = new util.ArrayList[(String, Object)]()
elements.add((JsonFields.Name, value.name))
Expand Down Expand Up @@ -288,6 +313,12 @@ object Config {
)
)

if (value.scripts.nonEmpty) {
elements.add(
(JsonFields.Scripts, scriptsEncoder.encode(value.scripts))
)
}

toMap(elements)
}
}
Expand Down
6 changes: 4 additions & 2 deletions lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,8 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) {
authors: List[Contact] = List(),
maintainers: List[Contact] = List(),
license: String = "",
componentGroups: Option[ComponentGroups] = None
componentGroups: Option[ComponentGroups] = None,
scripts: List[Script] = Nil
): Package[F] = {
val config = Config(
name = name,
Expand All @@ -300,7 +301,8 @@ class PackageManager[F](implicit val fileSystem: FileSystem[F]) {
edition = edition,
preferLocalLibraries = true,
maintainers = maintainers,
componentGroups = componentGroups
componentGroups = componentGroups,
scripts = scripts
)
create(root, config, template)
}
Expand Down
68 changes: 68 additions & 0 deletions lib/scala/pkg/src/main/scala/org/enso/pkg/Script.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package org.enso.pkg

import io.circe.{Decoder, DecodingFailure, Encoder, Json}
import org.enso.scala.yaml.{YamlDecoder, YamlEncoder}
import org.yaml.snakeyaml.error.YAMLException
import org.yaml.snakeyaml.nodes.{MappingNode, Node, ScalarNode, SequenceNode}

import java.util

case class Script(name: String, arguments: Seq[String])

object Script {

/** [[Encoder]] instance for the [[Script]]. */
implicit val encoder: Encoder[Script] = { script =>
val vs = script.arguments.map(Json.fromString)
Json.obj(script.name -> Json.arr(vs: _*))
}

implicit val yamlEncoder: YamlEncoder[Script] =
new YamlEncoder[Script] {
override def encode(value: Script) = {
val fields = new util.ArrayList[String](value.arguments.length)
value.arguments.foreach(v => fields.add(v))
toMap(value.name, fields)
}
}

/** [[Decoder]] instance for the [[Script]]. */
implicit val decoder: Decoder[Script] = { json =>
for {
key <- json.key.toRight(DecodingFailure("no key", Nil))
fields <- json.get[List[String]](key)
} yield Script(key, fields)
}

implicit val yamlDecoder: YamlDecoder[Script] =
new YamlDecoder[Script] {
override def decode(node: Node): Either[Throwable, Script] =
node match {
case mappingNode: MappingNode =>
if (mappingNode.getValue.size() == 1) {
val groupNode = mappingNode.getValue.get(0)
(groupNode.getKeyNode, groupNode.getValueNode) match {
case (scalarNode: ScalarNode, seqNode: SequenceNode) =>
val stringDecoder = implicitly[YamlDecoder[String]]
val valuesDecoder = implicitly[YamlDecoder[Seq[String]]]

for {
k <- stringDecoder.decode(scalarNode)
vs <- valuesDecoder.decode(seqNode)
} yield Script(k, vs)
case _ =>
Left(
new YAMLException(
"Failed to decode script. Expected a map field"
)
)
}
} else {
Left(
new YAMLException("Failed to decode script")
)
}
}
}

}
59 changes: 58 additions & 1 deletion lib/scala/pkg/src/test/scala/org/enso/pkg/ConfigSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ class ConfigSpec
Contact(None, Some("c@example.com"))
),
preferLocalLibraries = true,
componentGroups = None
componentGroups = None,
scripts =
List(Script("refresh", List("Standard.Base.Http.Caches.refresh")))
)
val deserialized = Config.fromYaml(config.toYaml).get
deserialized shouldEqual config
Expand Down Expand Up @@ -71,6 +73,61 @@ class ConfigSpec
}
}

"Scripts" should {
"correctly de-serialize and serialize back" in {
val config =
"""|name: FooBar
|namespace: local
|scripts:
|- refresh:
| - Standard.Base.Http.Caches.refresh
| - Standard.Base.Caches.refresh
|""".stripMargin
val parsed = Config.fromYaml(config).get

val expectedScripts = List(
Script(
"refresh",
List(
"Standard.Base.Http.Caches.refresh",
"Standard.Base.Caches.refresh"
)
)
)
parsed.scripts shouldEqual expectedScripts
val serialized = parsed.toYaml
serialized shouldEqual config
}

"reject duplicate entries" in {
val config =
"""|name: FooBar
|namespace: local
|scripts:
|- refresh:
| - Standard.Base.Http.Caches.refresh
|- refresh:
| - Standard.Base.Http.Caches.refresh
|""".stripMargin
val parsed = Config.fromYaml(config)
parsed.isFailure shouldBe true
parsed.failed.get.getMessage should include("Scripts have to be unique")
}

"accept unknown scripts" in {
val config =
"""|name: FooBar
|namespace: local
|scripts:
|- startup:
| - Standard.Base.Boot.start
|""".stripMargin
val parsed = Config.fromYaml(config)
parsed.isSuccess shouldBe true
parsed.get.scripts.head.name shouldBe "startup"
}
}

"Component groups" should {

"correctly de-serialize and serialize back the components syntax" in {
Expand Down
Loading