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

Register services in package.yaml instead via ServiceLoader #11868

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions distribution/lib/Standard/Base/0.0.0-dev/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ component-groups:
- Logical: { color: oklch(0.464 0.14 300) }
- Operators: { color: oklch(0.464 0.14 275) }
- Errors: { color: oklch(0.464 0.14 15) }

services:
Copy link
Member Author

Choose a reason for hiding this comment

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

To register an implementation one adds services section that specifies pair of SPI type and implementation type.

@jdunkerley: If you want the IDE to show a drop box with all available providers sooner than the library is loaded, then we might need (localized) display name explaining why such a library should be imported. Engine+ls would deliver list of all possible implementations to the IDE via suggestion db or etc.

Copy link
Member Author

@JaroslavTulach JaroslavTulach Dec 17, 2024

Choose a reason for hiding this comment

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

James would like Meta.lookup_services to be able to return even the not enabled services. Then one can create a drop down with a selection of all possible implementation. Question: what happens when a not enabled service is selected? How does one turn that library on?

Copy link
Member

Choose a reason for hiding this comment

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

Shall Meta provide something like Meta.is_library_imported? Then if not, we could give some feedback to the language server and ask it to insert a missing import.

Copy link
Member Author

@JaroslavTulach JaroslavTulach Jan 4, 2025

Choose a reason for hiding this comment

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

feedback to the language server

E.g. if we continue to insist on solving the problem with not enabled services we effectively put this PR on hold.

- provides: Standard.Base.System.File.File_System_SPI
with: Standard.Base.Enso_Cloud.Enso_File_System_Impl.Enso_File_System_Impl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
private

import project.System.File.File_System_SPI
import project.Enso_Cloud.Enso_File

type Enso_File_System_Impl
Copy link
Member Author

Choose a reason for hiding this comment

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

Registration of implementation can be done in a private module by defining a singleton type (which is its own type) and a conversion from such type to the SPI interface. In this case the conversion is using File_System_SPI.new factory method.


File_System_SPI.from (_:Enso_File_System_Impl) =
Copy link
Member

Choose a reason for hiding this comment

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

To ensure this works, you should remove EnsoPathFileSystemImpl.java?

File_System_SPI.new "enso" Enso_File
22 changes: 15 additions & 7 deletions distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,6 @@ polyglot java import java.nio.file.StandardOpenOption
polyglot java import java.time.ZonedDateTime
polyglot java import org.enso.base.DryRunFileManager
polyglot java import org.enso.base.file_system.File_Utils
polyglot java import org.enso.base.file_system.FileSystemSPI

## PRIVATE
file_types : Vector
file_types = Vector.from_polyglot_array (FileSystemSPI.get_types False)

## Represents a file or folder on the filesystem.
@Builtin_Type
Expand Down Expand Up @@ -81,13 +76,13 @@ type File
new path = case path of
_ : Text -> if path.contains "://" . not then resolve_path path else
protocol = path.split "://" . first
file_system = FileSystemSPI.get_type protocol False
file_system = File_System_SPI.get_type protocol False
if file_system.is_nothing then Error.throw (Illegal_Argument.Error "Unsupported protocol "+protocol) else
file_system.new path
_ : File -> path
_ ->
## Check to see if a valid "File" type.
if (file_types.any file_type-> path.is_a file_type) then path else
if ((File_System_SPI.get_types False).any file_type-> path.is_a file_type) then path else
Error.throw (Illegal_Argument.Error "The provided path is neither a Text, nor any recognized File-like type.")


Expand Down Expand Up @@ -924,3 +919,16 @@ local_file_move (source : File) (destination : File) (replace_existing : Boolean
File_Error.handle_java_exceptions source <|
copy_options = if replace_existing then [StandardCopyOption.REPLACE_EXISTING.to_text] else []
source.move_builtin destination copy_options

# PRIVATE
type File_System_SPI
Copy link
Member Author

Choose a reason for hiding this comment

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

First step, @radeusgd, is to define an SPI type. It can contain any information needed to process the registered instances. In this case it is protocol name. For purposes of simplicity the other argument is typ - to keep the implementation compatible, but one could do better, probably.

private Entry protocol:Text typ

new protocol:Text typ = File_System_SPI.Entry protocol typ

private get_type protocol:Text _:Boolean =
vec = Self.get_types . filter (_.protocol == protocol)
if vec . not_empty then vec.first else Nothing
JaroslavTulach marked this conversation as resolved.
Show resolved Hide resolved

private get_types _:Boolean =
Meta.lookup_services File_System_SPI
Copy link
Member Author

Choose a reason for hiding this comment

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

To look the implementations up one will be allowed to use lookup_services method and specify the SPI type one is interested in. The behavior of the lookup_services method will be similar to the operations shown in the MetaServicesTest.

Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.enso.interpreter.test.interop;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import java.net.URI;
import org.enso.interpreter.node.callable.InteropApplicationNode;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.UnresolvedConversion;
import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.state.State;
import org.enso.test.utils.ContextUtils;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

public class MetaServicesTest {
private static Context ctx;

@BeforeClass
public static void prepareCtx() {
ctx = ContextUtils.createDefaultContext();
}

@AfterClass
public static void disposeCtx() {
ctx.close();
ctx = null;
}

@Test
public void loadFileSystemServices() throws Exception {
final URI uri = new URI("memory://services.enso");
final Source src =
Source.newBuilder(
"enso",
"""
import Standard.Base.System.File.File_System_SPI
import Standard.Base.Meta
import Standard.Base.Enso_Cloud.Enso_File_System_Impl

data =
Meta.lookup_services File_System_SPI
""",
"services.enso")
.uri(uri)
.buildLiteral();

var mod = ctx.eval(src);
var ensoCtx = ContextUtils.leakContext(ctx);

for (var p : ensoCtx.getPackageRepository().getLoadedPackagesJava()) {
p.getConfig()
.services()
.foreach(
pw -> {
var spiType = findType(pw.provides(), ensoCtx);
var implType = findType(pw.with(), ensoCtx);
var fsImpl =
ContextUtils.executeInContext(
ctx,
() -> {
var conversion =
UnresolvedConversion.build(implType.getDefinitionScope());
var state = State.create(ensoCtx);
var node = InteropApplicationNode.getUncached();
var fn = conversion.resolveFor(ensoCtx, spiType, implType);
var conv = node.execute(fn, state, new Object[] {spiType, implType});
if (conv != null) {
return conv;
}
return null;
});
assertNotNull("Some implementation found", fsImpl);
assertEquals("Protocol", "enso", fsImpl.getMember("protocol").asString());
assertEquals(
"Type",
"Standard.Base.Enso_Cloud.Enso_File",
fsImpl.getMember("typ").getMetaQualifiedName());
return null;
});
}
}

private Type findType(String name, EnsoContext ensoCtx) {
var moduleName = name.replaceFirst("\\.[^\\.]*$", "");
var typeName = name.substring(moduleName.length() + 1);
var module = ensoCtx.getTopScope().getModule(moduleName).get();
var implType = module.getScope().getType(typeName, true);
return implType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ public static InteropApplicationNode build() {
return InteropApplicationNodeGen.create();
}

/**
* Returns shared, uncached instance of this node
*
* @return shared interop application node
*/
public static InteropApplicationNode getUncached() {
return InteropApplicationNodeGen.getUncached();
}

/**
* Calls the function with given state and arguments.
*
Expand Down
59 changes: 57 additions & 2 deletions lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,53 @@ import org.yaml.snakeyaml.nodes.{MappingNode, Node}
import java.io.{Reader, StringReader}
import java.util
import scala.util.Try
import java.io.IOException

/** Information about registered service.
*
* @param provides name of SPI type
* @param with name of implementation type
*/
case class ProvidesWith(val provides: String, val `with`: String) {}
Copy link
Member

Choose a reason for hiding this comment

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

Can we change String to org.enso.pkg.QualifiedName?


object ProvidesWith {

/** Fields for use when serializing the [[ProvidesWith]]. */
object Fields {
val Provides = "provides"
val With = "with"
}

implicit val decoderSnake: YamlDecoder[ProvidesWith] =
new YamlDecoder[ProvidesWith] {
override def decode(node: Node): Either[Throwable, ProvidesWith] =
node match {
case mappingNode: MappingNode =>
val str = implicitly[YamlDecoder[String]]
val bindings = mappingKV(mappingNode)
for {
p <- bindings
.get(Fields.Provides)
.map(str.decode)
.getOrElse(Left(new IOException("Missing `provides` field")))
w <- bindings
.get(Fields.With)
.map(str.decode)
.getOrElse(Left(new IOException("Missing `with` field")))
} yield ProvidesWith(p, w)
}
}

implicit val encoderSnake: YamlEncoder[ProvidesWith] =
new YamlEncoder[ProvidesWith] {
override def encode(value: ProvidesWith) = {
val elements = new util.ArrayList[(String, Object)]()
elements.add((Fields.Provides, value.provides))
elements.add((Fields.With, value.`with`))
toMap(elements)
}
}
}

/** Contact information to a user.
*
Expand Down Expand Up @@ -110,7 +157,8 @@ case class Config(
maintainers: List[Contact],
edition: Option[Editions.RawEdition],
preferLocalLibraries: Boolean,
componentGroups: Option[ComponentGroups]
componentGroups: Option[ComponentGroups],
services: List[ProvidesWith]
) {

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

implicit val yamlDecoder: YamlDecoder[Config] =
Expand All @@ -171,6 +220,7 @@ object Config {
val normalizedNameDecoder =
implicitly[YamlDecoder[Option[String]]]
val contactDecoder = implicitly[YamlDecoder[List[Contact]]]
val servicesDecoder = implicitly[YamlDecoder[List[ProvidesWith]]]
val editionNameDecoder = implicitly[YamlDecoder[EditionName]]
val editionDecoder =
implicitly[YamlDecoder[Option[Editions.RawEdition]]]
Expand Down Expand Up @@ -233,6 +283,10 @@ object Config {
.get(JsonFields.ComponentGroups)
.map(componentGroups.decode)
.getOrElse(Right(None))
services <- clazzMap
.get(JsonFields.Services)
.map(servicesDecoder.decode)
.getOrElse(Right(Nil))
} yield Config(
name,
normalizedName,
Expand All @@ -243,7 +297,8 @@ object Config {
maintainers,
edition,
preferLocalLibraries,
componentGroups
componentGroups,
services
)
}
}
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,
services: List[ProvidesWith] = List()
): 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,
services = services
)
create(root, config, template)
}
Expand Down
Loading