diff --git a/distribution/lib/Standard/Base/0.0.0-dev/package.yaml b/distribution/lib/Standard/Base/0.0.0-dev/package.yaml index 8c92eac4c74b..14647940afa5 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/package.yaml +++ b/distribution/lib/Standard/Base/0.0.0-dev/package.yaml @@ -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: + - provides: Standard.Base.System.File.File_System_SPI + with: Standard.Base.Enso_Cloud.Enso_File_System_Impl.Enso_File_System_Impl diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Enso_Cloud/Enso_File_System_Impl.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Enso_Cloud/Enso_File_System_Impl.enso new file mode 100644 index 000000000000..c538be479dfa --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Enso_Cloud/Enso_File_System_Impl.enso @@ -0,0 +1,9 @@ +private + +import project.System.File.File_System_SPI +import project.Enso_Cloud.Enso_File + +type Enso_File_System_Impl + +File_System_SPI.from (_:Enso_File_System_Impl) = + File_System_SPI.new "enso" Enso_File diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso index 825069dfc9ad..be3944ec2fd8 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso @@ -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 @@ -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.") @@ -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 + 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 + + private get_types _:Boolean = + Meta.lookup_services File_System_SPI diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/MetaServicesTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/MetaServicesTest.java new file mode 100644 index 000000000000..a56f6e97e0a0 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/MetaServicesTest.java @@ -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; + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/InteropApplicationNode.java b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/InteropApplicationNode.java index 0e3364a6899a..6610676fb216 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/node/callable/InteropApplicationNode.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/node/callable/InteropApplicationNode.java @@ -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. * diff --git a/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala b/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala index 3adb2ecfd257..b4705745f444 100644 --- a/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala +++ b/lib/scala/pkg/src/main/scala/org/enso/pkg/Config.scala @@ -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) {} + +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. * @@ -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. */ @@ -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] = @@ -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]]] @@ -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, @@ -243,7 +297,8 @@ object Config { maintainers, edition, preferLocalLibraries, - componentGroups + componentGroups, + services ) } } diff --git a/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala b/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala index 32279c2764b0..32d8c4b14a1e 100644 --- a/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala +++ b/lib/scala/pkg/src/main/scala/org/enso/pkg/Package.scala @@ -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, @@ -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) }