From 77a0e2117c4068e0c86f50ce1bacd47d1d07ead1 Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Fri, 16 Feb 2024 14:42:17 +0100 Subject: [PATCH] feat (core): Initial version of working $proto link on UI - yay! This commit is the accumulation of the work I've done over the past few week-ends; see https://github.com/enola-dev/enola/pull/451 for history. There is some unrelated early code in here which is related to more things to come soon-ish. --- .bazelrc | 1 + .devcontainer/devcontainer.json | 5 + .editorconfig | 3 + .github/dependabot.yml | 7 + .pre-commit-config.yaml | 39 ++++- .vscode/extensions.json | 8 +- .yamllint.yaml | 21 +++ .../java/dev/enola/cli/CommandWithModel.java | 7 +- .../io/mediatype/MarkdownMediaTypes.java | 52 ++++++ .../io/mediatype/MediaTypeProvider.java | 3 + .../common/io/mediatype/YamlMediaType.java | 4 +- .../resource/DelegatingMultipartResource.java | 50 ++++++ .../common/io/resource/EmptyResource.java | 22 ++- .../common/io/resource/MarkdownResource.java | 64 ++++++++ .../common/io/resource/MultipartResource.java | 58 +++++++ .../common/io/resource/NullResource.java | 4 +- ...dableButNotWritableDelegatingResource.java | 26 +++ .../ReadableButNotWritableResource.java | 29 ++++ .../io/resource/RegexMultipartResource.java | 100 ++++++++++++ .../common/io/resource/ResourceProviders.java | 24 +-- .../common/io/resource/StringResource.java | 51 +++++- .../common/protobuf/ProtobufMediaTypes.java | 6 +- .../io/resource/MarkdownResourceTest.java | 114 ++++++++++++++ .../io/resource/StringResourceTest.java | 6 +- .../java/dev/enola/common/protobuf/Anys.java | 56 ------- .../common/protobuf/DescriptorProvider.java | 4 + .../TypeRegistryDescriptorProvider.java | 5 + .../dev/enola/core/EnolaServiceProvider.java | 116 +++++++++++++- .../dev/enola/core/EnolaServiceRegistry.java | 5 + .../java/dev/enola/core/EntityAspect.java | 4 +- .../dev/enola/core/EntityAspectRepeater.java | 2 + .../dev/enola/core/EntityAspectService.java | 1 + .../main/java/dev/enola/core/Repository.java | 32 ++++ .../dev/enola/core/RepositoryBuilder.java | 68 ++++++++ .../enola/core/aspects/ErrorTestAspect.java | 2 + .../aspects/FilestoreRepositoryAspect.java | 2 + .../dev/enola/core/aspects/GrpcAspect.java | 3 + .../enola/core/aspects/TimestampAspect.java | 2 + .../enola/core/aspects/UriTemplateAspect.java | 2 + .../enola/core/aspects/ValidationAspect.java | 2 + .../dev/enola/core/meta/SchemaAspect.java | 2 +- .../enola/core/meta/TypeRegistryWrapper.java | 9 +- .../dev/enola/core/thing/ThingConnector.java | 42 +++++ .../core/thing/ThingConnectorService.java | 68 ++++++++ .../dev/enola/core/type/ProtoService.java | 73 +++++++++ .../core/type/TypeRepositoryBuilder.java | 46 ++++++ .../enola/core/meta/TypeRepositoryTest.java | 71 +++++++++ core/impl/src/test/resources/test-types.yaml | 28 ++++ core/lib/BUILD | 31 +++- .../main/java/dev/enola/core/enola_core.proto | 3 +- .../main/java/dev/enola/core/enola_ext.proto | 33 ++++ .../java/dev/enola/core/meta/enola_meta.proto | 148 +++++++++++++++++- .../main/java/dev/enola/core/view/Things.java | 2 +- docs/concepts/core.md | 10 +- docs/concepts/uri.md | 4 +- docs/dev/setup.md | 11 +- docs/use/library/model.yaml | 2 +- docs/use/rosetta/index.md | 6 +- 58 files changed, 1464 insertions(+), 135 deletions(-) create mode 100644 .yamllint.yaml create mode 100644 common/common/src/main/java/dev/enola/common/io/mediatype/MarkdownMediaTypes.java create mode 100644 common/common/src/main/java/dev/enola/common/io/resource/DelegatingMultipartResource.java create mode 100644 common/common/src/main/java/dev/enola/common/io/resource/MarkdownResource.java create mode 100644 common/common/src/main/java/dev/enola/common/io/resource/MultipartResource.java create mode 100644 common/common/src/main/java/dev/enola/common/io/resource/ReadableButNotWritableDelegatingResource.java create mode 100644 common/common/src/main/java/dev/enola/common/io/resource/ReadableButNotWritableResource.java create mode 100644 common/common/src/main/java/dev/enola/common/io/resource/RegexMultipartResource.java create mode 100644 common/common/src/test/java/dev/enola/common/io/resource/MarkdownResourceTest.java delete mode 100644 common/protobuf/src/main/java/dev/enola/common/protobuf/Anys.java create mode 100644 core/impl/src/main/java/dev/enola/core/Repository.java create mode 100644 core/impl/src/main/java/dev/enola/core/RepositoryBuilder.java create mode 100644 core/impl/src/main/java/dev/enola/core/thing/ThingConnector.java create mode 100644 core/impl/src/main/java/dev/enola/core/thing/ThingConnectorService.java create mode 100644 core/impl/src/main/java/dev/enola/core/type/ProtoService.java create mode 100644 core/impl/src/main/java/dev/enola/core/type/TypeRepositoryBuilder.java create mode 100644 core/impl/src/test/java/dev/enola/core/meta/TypeRepositoryTest.java create mode 100644 core/impl/src/test/resources/test-types.yaml create mode 100644 core/lib/src/main/java/dev/enola/core/enola_ext.proto diff --git a/.bazelrc b/.bazelrc index 8dc656b23..cc09eea03 100644 --- a/.bazelrc +++ b/.bazelrc @@ -4,6 +4,7 @@ common --enable_bzlmod common --extra_toolchains=@local_jdk//:all +# Java version must match .devcontainer/devcontainer.json # https://bazel.build/docs/bazel-and-java#java-versions build --java_language_version=21 build --tool_java_language_version=21 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0ec85093e..0ab81ae6c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -22,8 +22,13 @@ // Features to add to the dev container. More info: https://containers.dev/features. "features": { + // Java version must match .bazelrc "ghcr.io/devcontainers/features/java:1": { "version": "21" + }, + // protoc is used by tools/protoc/protoc.bash + "ghcr.io/devcontainers-contrib/features/protoc-asdf:1": { + "version": "3.6.1" } }, diff --git a/.editorconfig b/.editorconfig index ab7e8c8c6..ca6df1d83 100644 --- a/.editorconfig +++ b/.editorconfig @@ -47,14 +47,17 @@ max_line_length = unset max_line_length = unset indent_size = 2 tab_width = 2 +max_line_length = unset [*.jsonc] indent_size = 2 tab_width = 2 +max_line_length = unset [*.json5] indent_size = 2 tab_width = 2 +max_line_length = unset [*.toml] indent_size = 2 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 41ac9d138..841ec590f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,3 +14,10 @@ updates: day: monday time: "04:00" open-pull-requests-limit: 99 + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: "monthly" + day: monday + time: "04:00" + open-pull-requests-limit: 99 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aebdcdc7c..24e6002c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -152,13 +152,44 @@ repos: hooks: - id: csslint - # https://editorconfig.org check should run AFTER all of the formatters (above) - - repo: https://github.com/editorconfig-checker/editorconfig-checker.python - rev: 2.7.3 + # https://yamllint.readthedocs.io/en/stable/integration.html#integration-with-pre-commit + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.33.0 hooks: - - id: editorconfig-checker + - id: yamllint + # https://yamllint.readthedocs.io/en/stable/configuration.html#errors-and-warnings + args: [--strict] - repo: https://github.com/shellcheck-py/shellcheck-py rev: v0.9.0.6 hooks: - id: shellcheck + + - repo: https://github.com/python-jsonschema/check-jsonschema + rev: 0.28.0 + hooks: + - id: check-github-actions + args: ["--verbose"] + - id: check-github-workflows + args: ["--verbose"] + - id: check-dependabot + args: ["--verbose"] + - id: check-renovate + args: ["--verbose"] + - id: check-metaschema + files: \.schema\.json$ + args: ["--verbose"] + # TODO Change once https://github.com/python-jsonschema/check-jsonschema/issues/340 is implemented + - id: check-jsonschema + # files: .+/models/.+\.(yaml|json)$ + files: \.types\.yaml$ + args: ["--verbose", "--schemafile", "docs/models/enola/schemas/Types.schema.json"] + - id: check-jsonschema + files: \.type\.yaml$ + args: ["--verbose", "--schemafile", "docs/models/enola/schemas/Type.schema.json"] + + # https://editorconfig.org check should run AFTER all of the formatters (above) + - repo: https://github.com/editorconfig-checker/editorconfig-checker.python + rev: 2.7.3 + hooks: + - id: editorconfig-checker diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 5b184452c..99ebe88be 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -16,6 +16,10 @@ "zignd.html-css-class-completion", "timonwong.shellcheck" ], - // https://github.com/kshetline/ligatures-limited/issues/39 - "unwantedRecommendations": ["kshetline.ligatures-limited"] + "unwantedRecommendations": [ + // https://github.com/kshetline/ligatures-limited/issues/39 + "kshetline.ligatures-limited", + "vscjava.vscode-gradle", + "vscjava.vscode-maven" + ] } diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 000000000..7996874f2 --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2024 The Enola Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# https://yamllint.readthedocs.io/en/stable/configuration.html + +rules: + # Let's just check this only via .editorconfig instead + line-length: disable diff --git a/cli/src/main/java/dev/enola/cli/CommandWithModel.java b/cli/src/main/java/dev/enola/cli/CommandWithModel.java index f39ed7631..ead5c93fb 100644 --- a/cli/src/main/java/dev/enola/cli/CommandWithModel.java +++ b/cli/src/main/java/dev/enola/cli/CommandWithModel.java @@ -19,11 +19,14 @@ import dev.enola.common.io.resource.ResourceProviders; import dev.enola.core.EnolaServiceProvider; +import dev.enola.core.Repository; import dev.enola.core.grpc.EnolaGrpcClientProvider; import dev.enola.core.grpc.EnolaGrpcInProcess; import dev.enola.core.grpc.ServiceProvider; import dev.enola.core.meta.EntityKindRepository; +import dev.enola.core.meta.proto.Type; import dev.enola.core.proto.EnolaServiceGrpc.EnolaServiceBlockingStub; +import dev.enola.core.type.TypeRepositoryBuilder; import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Model.CommandSpec; @@ -54,7 +57,9 @@ public final void run() throws Exception { var modelResource = new ResourceProviders().getReadableResource(group.model); ekr = new EntityKindRepository(); ekr.load(modelResource); - esp = new EnolaServiceProvider(ekr); + Repository tyr = new TypeRepositoryBuilder().build(); + // TODO --types for Types (and more?), e.g. from MD, YAML, textproto, etc. + esp = new EnolaServiceProvider(ekr, tyr); var enolaService = esp.getEnolaService(); grpc = new EnolaGrpcInProcess(esp, enolaService, false); // direct, single-threaded! gRPCService = grpc.get(); diff --git a/common/common/src/main/java/dev/enola/common/io/mediatype/MarkdownMediaTypes.java b/common/common/src/main/java/dev/enola/common/io/mediatype/MarkdownMediaTypes.java new file mode 100644 index 000000000..b6ce42850 --- /dev/null +++ b/common/common/src/main/java/dev/enola/common/io/mediatype/MarkdownMediaTypes.java @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.enola.common.io.mediatype; + +import static com.google.common.net.MediaType.create; + +import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.net.MediaType; + +import java.util.Map; +import java.util.Set; + +/** + * The "text/markdown" media type, as per RFC + * 7763 (and RFC 7764). + */ +public class MarkdownMediaTypes implements MediaTypeProvider { + + // TODO Distinguish https://commonmark.org from GFH et al. via a variant parameter; see + // https://www.iana.org/assignments/markdown-variants/markdown-variants.xhtml + + public static final MediaType MARKDOWN_UTF_8 = + create("text", "markdown").withCharset(Charsets.UTF_8); + ; + + @Override + public Map extensionsToTypes() { + return ImmutableMap.of("md", MARKDOWN_UTF_8); + } + + @Override + public Map> knownTypesWithAlternatives() { + return ImmutableMap.of(MARKDOWN_UTF_8, ImmutableSet.of(create("text", "x-markdown"))); + } +} diff --git a/common/common/src/main/java/dev/enola/common/io/mediatype/MediaTypeProvider.java b/common/common/src/main/java/dev/enola/common/io/mediatype/MediaTypeProvider.java index 7f25b8044..50e809304 100644 --- a/common/common/src/main/java/dev/enola/common/io/mediatype/MediaTypeProvider.java +++ b/common/common/src/main/java/dev/enola/common/io/mediatype/MediaTypeProvider.java @@ -23,6 +23,9 @@ import java.util.Set; public interface MediaTypeProvider { + + // TODO An implementation based on enola.dev/mediaType Type YAML/binary! + Map> knownTypesWithAlternatives(); Map extensionsToTypes(); diff --git a/common/common/src/main/java/dev/enola/common/io/mediatype/YamlMediaType.java b/common/common/src/main/java/dev/enola/common/io/mediatype/YamlMediaType.java index cc6f3dc3f..620bcdbfb 100644 --- a/common/common/src/main/java/dev/enola/common/io/mediatype/YamlMediaType.java +++ b/common/common/src/main/java/dev/enola/common/io/mediatype/YamlMediaType.java @@ -21,7 +21,7 @@ import com.google.common.base.Charsets; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Sets; +import com.google.common.collect.ImmutableSet; import com.google.common.net.MediaType; import java.util.Map; @@ -41,7 +41,7 @@ public class YamlMediaType implements MediaTypeProvider { public Map> knownTypesWithAlternatives() { return ImmutableMap.of( YAML_UTF_8, - Sets.newHashSet( + ImmutableSet.of( create("text", "yaml"), create("text", "x-yaml"), create("application", "x-yaml"))); diff --git a/common/common/src/main/java/dev/enola/common/io/resource/DelegatingMultipartResource.java b/common/common/src/main/java/dev/enola/common/io/resource/DelegatingMultipartResource.java new file mode 100644 index 000000000..e37bbd6c3 --- /dev/null +++ b/common/common/src/main/java/dev/enola/common/io/resource/DelegatingMultipartResource.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.enola.common.io.resource; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import java.io.IOException; + +public class DelegatingMultipartResource extends ReadableButNotWritableDelegatingResource + implements MultipartResource { + + private final ImmutableMap parts; + + public DelegatingMultipartResource( + ReadableResource baseResource, ImmutableMap parts) + throws IOException { + super(baseResource); + this.parts = parts; + } + + @Override + public ImmutableSet parts() { + return parts.keySet(); + } + + @Override + public Resource part(String name) { + var r = parts.get(name); + if (r == null) + throw new IllegalArgumentException( + "Only " + parts().toString() + ", invalid part: " + name); + else return r; + } +} diff --git a/common/common/src/main/java/dev/enola/common/io/resource/EmptyResource.java b/common/common/src/main/java/dev/enola/common/io/resource/EmptyResource.java index ae161a316..dfaf1651a 100644 --- a/common/common/src/main/java/dev/enola/common/io/resource/EmptyResource.java +++ b/common/common/src/main/java/dev/enola/common/io/resource/EmptyResource.java @@ -22,30 +22,38 @@ import com.google.common.net.MediaType; import java.net.URI; +import java.util.function.Supplier; /** - * Resources which when read is always immediately EOF. Note that this is read-only, and - * intentionally does not implement WritableResource; use e.g. {@link ResourceProviders#getResource} - * with "empty:-" to get a wrapped implementation that implements writable but throws an error. + * Read-only resources which when read are always immediately EOF. This is a bit like /dev/null on + * *NIX OS for reading, but not for writing (because /dev/null ignores writes, whereas this fails). * - * @see NullResource for an alternatives that returns 0s instead of EOF. + * @see NullResource for an alternatives that returns infinite 0s instead of EOF. */ -public class EmptyResource implements ReadableResource { +public class EmptyResource implements ReadableButNotWritableResource { // TODO Perhaps rename this to VoidResource with void:/ URI? static final String SCHEME = "empty"; private static final URI EMPTY_URI = URI.create(SCHEME + ":?"); - private final MediaType mediaType; - private final URI uri; + private final MediaType mediaType; + private final Supplier uriSupplier; + private URI uri; public EmptyResource(MediaType mediaType) { this.mediaType = mediaType; this.uri = URIs.addMediaType(EMPTY_URI, mediaType); + this.uriSupplier = null; + } + + public EmptyResource(MediaType mediaType, Supplier uriSupplier) { + this.mediaType = mediaType; + this.uriSupplier = uriSupplier; } @Override public URI uri() { + if (uri == null) uri = uriSupplier.get(); return uri; } diff --git a/common/common/src/main/java/dev/enola/common/io/resource/MarkdownResource.java b/common/common/src/main/java/dev/enola/common/io/resource/MarkdownResource.java new file mode 100644 index 000000000..2ff28f0e4 --- /dev/null +++ b/common/common/src/main/java/dev/enola/common/io/resource/MarkdownResource.java @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.enola.common.io.resource; + +import static com.google.common.net.MediaType.HTML_UTF_8; + +import static dev.enola.common.io.mediatype.YamlMediaType.YAML_UTF_8; + +import com.google.common.collect.ImmutableMap; + +import java.io.IOException; + +/** + * MarkdownResource is a {@link MultipartResource} which separates "Front Matter" (as {@link #FRONT} + * part, typically YAML structured data) and Markdown content (as {@link #BODY}) from the base + * resource. + * + *

The "frontmatter" is anything between 2 "---" lines at the very start of the file (if present, + * it's optional) and the "body" is everything that follows. Any HTML comment before the frontmatter + * is stripped (this is useful e.g. to ignore license headers). + * + *

This is very common de-facto standard format used by many Markdown tools. It may (TBC) + * originally have been introduced by Jekyll. + */ +public class MarkdownResource extends RegexMultipartResource { + + public static final String FIRST_COMMENT = "comment"; + public static final String FRONT = "frontmatter"; + public static final String BODY = "body"; + + private static final PartsDef PARTS = + new PartsDef( + "(?s)^(?<" + + FIRST_COMMENT + + ">)?(\r?\n)*(===\r?\n(?<" + + FRONT + + ">.*)===\r?\n)?(\r?\n)*(?<" + + BODY + + ">.*)$", + ImmutableMap.of( + FIRST_COMMENT, + HTML_UTF_8.withoutParameters(), + FRONT, + YAML_UTF_8.withoutParameters())); + + public MarkdownResource(ReadableResource baseResource) throws IOException { + super(baseResource, PARTS); + } +} diff --git a/common/common/src/main/java/dev/enola/common/io/resource/MultipartResource.java b/common/common/src/main/java/dev/enola/common/io/resource/MultipartResource.java new file mode 100644 index 000000000..2d3e7c5dd --- /dev/null +++ b/common/common/src/main/java/dev/enola/common/io/resource/MultipartResource.java @@ -0,0 +1,58 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.enola.common.io.resource; + +import com.google.common.collect.ImmutableSet; +import com.google.common.net.MediaType; + +import java.net.URI; + +/** + * MultipartResources are "logical" resources which do have an URI, but are composed of multiple + * "parts" which are independent (sub)resources, each with their own {@link MediaType} and content. + * + *

Examples could be Emails with attachments (RFC 2045 & 2046), or files on operating systems + * with file systems where a single file can contain multiple alternative data streams (e.g. NTFS, + * and old classic Mac OS's HFS with data & resources forks), or things like the {@link + * FrontmatterResource}. + * + *

The URI of the sub-resources should correspond to the "parent" MultipartResource's URI + * appended by the part name as a #fragment. + * + *

This interface intentionally does not extend {@link Resource}, because not all implementations + * will have a "root" resource (although some may). + */ +public interface MultipartResource { + + MediaType MEDIA_TYPE = MediaType.create("multipart", "related"); + + /** + * MediaType defaults to {@link #MEDIA_TYPE}. Implementations may or may not return media types + * with charset parameters, as that's also specified on parts, and the encoding may or may not + * be the same for each of them. + */ + default MediaType mediaType() { + return MEDIA_TYPE; + } + + URI uri(); + + ImmutableSet parts(); + + Resource part(String name); +} diff --git a/common/common/src/main/java/dev/enola/common/io/resource/NullResource.java b/common/common/src/main/java/dev/enola/common/io/resource/NullResource.java index efdda9c1f..c70b17edd 100644 --- a/common/common/src/main/java/dev/enola/common/io/resource/NullResource.java +++ b/common/common/src/main/java/dev/enola/common/io/resource/NullResource.java @@ -30,8 +30,8 @@ /** * Resource which ignores writes, and returns an infinite amount of bytes of value 0 on read. This - * is a bit like /dev/null on *NIX OS, but not quite (because /dev/null returns EOF on read; this - * does not). + * is a bit like /dev/null on *NIX OS for writing, but not for reading (because /dev/null returns + * EOF on read, but this does not). * * @see EmptyResource for an (non-writable) EOF ReadableResource */ diff --git a/common/common/src/main/java/dev/enola/common/io/resource/ReadableButNotWritableDelegatingResource.java b/common/common/src/main/java/dev/enola/common/io/resource/ReadableButNotWritableDelegatingResource.java new file mode 100644 index 000000000..238d8104d --- /dev/null +++ b/common/common/src/main/java/dev/enola/common/io/resource/ReadableButNotWritableDelegatingResource.java @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.enola.common.io.resource; + +public class ReadableButNotWritableDelegatingResource extends DelegatingReadableResource + implements ReadableButNotWritableResource { + + public ReadableButNotWritableDelegatingResource(ReadableResource resource) { + super(resource); + } +} diff --git a/common/common/src/main/java/dev/enola/common/io/resource/ReadableButNotWritableResource.java b/common/common/src/main/java/dev/enola/common/io/resource/ReadableButNotWritableResource.java new file mode 100644 index 000000000..0febb397f --- /dev/null +++ b/common/common/src/main/java/dev/enola/common/io/resource/ReadableButNotWritableResource.java @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.enola.common.io.resource; + +import com.google.common.io.ByteSink; + +public interface ReadableButNotWritableResource extends Resource { + + @Override + default ByteSink byteSink() { + throw new UnsupportedOperationException( + "This is a read-only resource which is not writable."); + } +} diff --git a/common/common/src/main/java/dev/enola/common/io/resource/RegexMultipartResource.java b/common/common/src/main/java/dev/enola/common/io/resource/RegexMultipartResource.java new file mode 100644 index 000000000..dab80ea75 --- /dev/null +++ b/common/common/src/main/java/dev/enola/common/io/resource/RegexMultipartResource.java @@ -0,0 +1,100 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.enola.common.io.resource; + +import com.google.common.collect.ImmutableMap; +import com.google.common.net.MediaType; + +import java.io.IOException; +import java.net.URI; +import java.util.HashSet; +import java.util.Map; +import java.util.function.Supplier; +import java.util.regex.Pattern; + +/** + * Resources which "splits" a "base" resource into "parts", based on Regular Expression named + * capturing groups. + */ +public class RegexMultipartResource extends DelegatingMultipartResource { + + public RegexMultipartResource(ReadableResource baseResource, PartsDef defs) throws IOException { + super(baseResource, split(baseResource, defs)); + } + + protected static record PartsDef(String regex, Map mediaTypes) {} + ; + + private static ImmutableMap split( + ReadableResource baseResource, PartsDef defs) throws IOException { + // TODO #performance Refactor this, so that e.g. MarkdownResource can compile the Pattern + // once only + var pattern = Pattern.compile(defs.regex, Pattern.DOTALL | Pattern.MULTILINE); + var groups = pattern.namedGroups(); + var keys = groups.keySet(); + + var text = baseResource.charSource().read(); + var matcher = pattern.matcher(text); + + var n = groups.size(); + var usedPartNames = new HashSet(n); + var parts = ImmutableMap.builderWithExpectedSize(n); + if (matcher.find()) { + for (var key : keys) { + String part = matcher.group(key); + var fragment = partFragmentURI(baseResource, key); + var mediaType = mediaType(baseResource, defs.mediaTypes.get(key), key); + var resource = StringResource.of(part, mediaType, fragment); + parts.put(key, resource); + usedPartNames.add(key); + } + } + + for (var key : keys) { + if (!usedPartNames.contains(key)) { + var fragment = partFragmentURI(baseResource, key); + var mediaType = mediaType(baseResource, defs.mediaTypes.get(key), key); + var empty = new EmptyResource(mediaType, fragment); + parts.put(key, empty); + } + } + + return parts.build(); + } + + private static Supplier partFragmentURI(ReadableResource baseResource, String key) { + return () -> URI.create(baseResource.uri().toString() + "#" + key); + } + + private static MediaType mediaType( + ReadableResource baseResource, MediaType partMediaType, String key) { + var baseMediaType = baseResource.mediaType(); + if (partMediaType == null) partMediaType = baseMediaType; + if (!partMediaType.charset().isPresent()) { + var baseCharset = baseMediaType.charset(); + if (baseCharset.isPresent()) + partMediaType = partMediaType.withCharset(baseCharset.get()); + else { + var uri = baseResource.uri().toString() + "#" + key; + throw new IllegalArgumentException( + "Missing Charset on both base and part resources for: " + uri); + } + } + return partMediaType; + } +} diff --git a/common/common/src/main/java/dev/enola/common/io/resource/ResourceProviders.java b/common/common/src/main/java/dev/enola/common/io/resource/ResourceProviders.java index eac549266..838c3dc55 100644 --- a/common/common/src/main/java/dev/enola/common/io/resource/ResourceProviders.java +++ b/common/common/src/main/java/dev/enola/common/io/resource/ResourceProviders.java @@ -18,7 +18,6 @@ package dev.enola.common.io.resource; import com.google.common.base.Strings; -import com.google.common.io.ByteSink; import com.google.common.net.MediaType; import java.net.MalformedURLException; @@ -56,16 +55,15 @@ public Resource getResource(URI uri) { } else { cpr = new ClasspathResource(uriPath); } - return new ReadableButNotWritableResource(cpr); + return new ReadableButNotWritableDelegatingResource(cpr); } else if (scheme.startsWith(StringResource.SCHEME)) { // NOT new StringResource(uriPath, mediaType), // because that is confusing, as it will chop off after # and interpret '?' // which is confusing for users, for this URI scheme. If "literal" resources // WITH MediaType are required, consider adding DataResource for data: - var stringResource = new StringResource(uri.getSchemeSpecificPart()); - return new ReadableButNotWritableResource(stringResource); + return new StringResource(uri.getSchemeSpecificPart()); } else if (scheme.startsWith(EmptyResource.SCHEME)) { - return new ReadableButNotWritableResource(new EmptyResource(mediaType)); + return new EmptyResource(mediaType); } else if (scheme.startsWith(NullResource.SCHEME)) { return NullResource.INSTANCE; } else if (scheme.startsWith(ErrorResource.SCHEME)) { @@ -73,7 +71,7 @@ public Resource getResource(URI uri) { } else if (scheme.startsWith("http")) { try { // TODO Replace UrlResource with alternative, when implemented - return new ReadableButNotWritableResource(new UrlResource(uri.toURL())); + return new ReadableButNotWritableDelegatingResource(new UrlResource(uri.toURL())); } catch (MalformedURLException e) { throw new IllegalArgumentException("Malformed URI is not valid URL" + uri, e); } @@ -84,18 +82,4 @@ public Resource getResource(URI uri) { } throw new IllegalArgumentException("Unknown URI scheme '" + scheme + "' in: " + uri); } - - private static class ReadableButNotWritableResource extends DelegatingReadableResource - implements Resource { - - ReadableButNotWritableResource(ReadableResource resource) { - super(resource); - } - - @Override - public ByteSink byteSink() { - throw new UnsupportedOperationException( - "This is a read-only resource which is not writable."); - } - } } diff --git a/common/common/src/main/java/dev/enola/common/io/resource/StringResource.java b/common/common/src/main/java/dev/enola/common/io/resource/StringResource.java index d8cf69717..f3f1d00b6 100644 --- a/common/common/src/main/java/dev/enola/common/io/resource/StringResource.java +++ b/common/common/src/main/java/dev/enola/common/io/resource/StringResource.java @@ -24,41 +24,76 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Objects; +import java.util.function.Supplier; -public class StringResource implements ReadableResource { +public class StringResource implements ReadableButNotWritableResource { static final String SCHEME = "string"; private final String string; private final MediaType mediaType; - private final URI uri; + private final Supplier uriSupplier; + private URI uri; + public static Resource of(String text, MediaType mediaType, Supplier fragmentSupplier) { + if (text == null || text.isBlank()) { + return new EmptyResource(mediaType, fragmentSupplier); + } else { + return new StringResource(text, mediaType, fragmentSupplier); + } + } + + public static Resource of(String text, MediaType mediaType) { + if (text == null || text.isBlank()) { + return new EmptyResource(mediaType); + } else { + return new StringResource(text, mediaType); + } + } + + public static Resource of(String text) { + return of(text, MediaType.PLAIN_TEXT_UTF_8); + } + + @Deprecated // Use #of() instead! (Remove this.) public StringResource(String text) { this(text, MediaType.PLAIN_TEXT_UTF_8); } + @Deprecated // Use #of() instead! (Make protected instead public) public StringResource(String text, MediaType mediaType) { + this( + text, + mediaType, + () -> { + try { + return new URI(SCHEME, text, null); + } catch (URISyntaxException e) { + // This should never happen, if the escaping above is correct... + throw new IllegalArgumentException("String is invalid in URI: " + text, e); + } + }); + } + + protected StringResource(String text, MediaType mediaType, Supplier uriSupplier) { this.string = Objects.requireNonNull(text, "text"); if ("".equals(text)) { throw new IllegalArgumentException( "Empty string: not supported (because that's an invalid URI)"); } + this.mediaType = Objects.requireNonNull(mediaType, "mediaType"); if (!mediaType.charset().isPresent()) { throw new IllegalArgumentException( "MediaType is missing required charset: " + mediaType); } - try { - this.uri = new URI(SCHEME, string, null); - } catch (URISyntaxException e) { - // This should never happen, if the escaping above is correct... - throw new IllegalArgumentException("String is invalid in URI: " + text, e); - } + this.uriSupplier = uriSupplier; } @Override public URI uri() { + if (uri == null) uri = uriSupplier.get(); return uri; } diff --git a/common/common/src/main/java/dev/enola/common/protobuf/ProtobufMediaTypes.java b/common/common/src/main/java/dev/enola/common/protobuf/ProtobufMediaTypes.java index eeb7c8a6c..c83829f0c 100644 --- a/common/common/src/main/java/dev/enola/common/protobuf/ProtobufMediaTypes.java +++ b/common/common/src/main/java/dev/enola/common/protobuf/ProtobufMediaTypes.java @@ -21,7 +21,7 @@ import com.google.common.base.Charsets; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Sets; +import com.google.common.collect.ImmutableSet; import com.google.common.net.MediaType; import dev.enola.common.io.mediatype.MediaTypeProvider; @@ -30,7 +30,7 @@ import java.util.Set; public class ProtobufMediaTypes implements MediaTypeProvider { - // TODO move this class into the (TBD) common.proto module + // TODO move this class into the common.proto module! // TODO Introduce parameters like messageType to indicate .textproto root message type? (And // "sniffing" them.) @@ -68,7 +68,7 @@ public Map> knownTypesWithAlternatives() { emptySet(), PROTOBUF_BINARY, // https://stackoverflow.com/questions/30505408/what-is-the-correct-protobuf-content-type - Sets.newHashSet( + ImmutableSet.of( MediaType.create("application", "x-protobuf"), MediaType.create("application", "vnd.google.protobuf"))); } diff --git a/common/common/src/test/java/dev/enola/common/io/resource/MarkdownResourceTest.java b/common/common/src/test/java/dev/enola/common/io/resource/MarkdownResourceTest.java new file mode 100644 index 000000000..e4618323a --- /dev/null +++ b/common/common/src/test/java/dev/enola/common/io/resource/MarkdownResourceTest.java @@ -0,0 +1,114 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.enola.common.io.resource; + +import static com.google.common.truth.Truth.assertThat; + +import static dev.enola.common.io.mediatype.MarkdownMediaTypes.MARKDOWN_UTF_8; +import static dev.enola.common.io.mediatype.YamlMediaType.YAML_UTF_8; +import static dev.enola.common.io.resource.MarkdownResource.BODY; +import static dev.enola.common.io.resource.MarkdownResource.FRONT; + +import org.junit.Test; + +import java.io.IOException; +import java.net.URI; + +public class MarkdownResourceTest { + + String COMMENT = + """ + + + """; + + String FRONTMATTER = + """ + === + title: First Blog post! + === + + """; + + String MD = """ + # Thaw Blough! + + **It rocks...** + """; + + @Test + public void commentFrontmatterMarkdown() throws IOException { + var r = new MarkdownResource(StringResource.of(COMMENT + FRONTMATTER + MD, MARKDOWN_UTF_8)); + var f = r.part(FRONT); + var b = r.part(BODY); + + assertThat(b.charSource().read()).isEqualTo("# Thaw Blough!\n\n**It rocks...**\n"); + assertThat(f.charSource().read()).isEqualTo("title: First Blog post!\n"); + + assertThat(r.mediaType()).isEqualTo(MARKDOWN_UTF_8); + assertThat(f.mediaType()).isEqualTo(YAML_UTF_8); + assertThat(b.mediaType()).isEqualTo(MARKDOWN_UTF_8); + + check(f.uri(), r.uri(), FRONT); + check(b.uri(), r.uri(), BODY); + } + + private void check(URI uri, URI base, String fragment) { + assertThat(uri.getFragment()).isEqualTo(fragment); + assertThat(uri.toString()).startsWith(base.toString()); + } + + @Test + public void frontmatterAndMarkdownButNoComment() throws IOException { + var r = new MarkdownResource(StringResource.of(FRONTMATTER + MD, MARKDOWN_UTF_8)); + var f = r.part(FRONT); + var b = r.part(BODY); + + assertThat(b.charSource().read()).isEqualTo("# Thaw Blough!\n\n**It rocks...**\n"); + assertThat(f.charSource().read()).isEqualTo("title: First Blog post!\n"); + } + + @Test + public void onlyMarkdown() throws IOException { + var r = new MarkdownResource(StringResource.of(MD, MARKDOWN_UTF_8)); + assertThat(r.part(BODY).charSource().read()) + .isEqualTo("# Thaw Blough!\n\n**It rocks...**\n"); + assertThat(r.part(FRONT).charSource().read()).isEmpty(); + } + + @Test + public void onlyFrontmatter() throws IOException { + var r = new MarkdownResource(StringResource.of(FRONTMATTER, MARKDOWN_UTF_8)); + assertThat(r.part(BODY).charSource().read()).isEmpty(); + assertThat(r.part(FRONT).charSource().read()).isEqualTo("title: First Blog post!\n"); + } + + @Test + public void empty() throws IOException { + var r = new MarkdownResource(new EmptyResource(MARKDOWN_UTF_8)); + assertThat(r.part(BODY).charSource().read()).isEmpty(); + assertThat(r.part(FRONT).charSource().read()).isEmpty(); + assertThat(r.parts()).hasSize(3); + } +} diff --git a/common/common/src/test/java/dev/enola/common/io/resource/StringResourceTest.java b/common/common/src/test/java/dev/enola/common/io/resource/StringResourceTest.java index fa39f6b9b..32caecaca 100644 --- a/common/common/src/test/java/dev/enola/common/io/resource/StringResourceTest.java +++ b/common/common/src/test/java/dev/enola/common/io/resource/StringResourceTest.java @@ -27,13 +27,15 @@ public class StringResourceTest { @Test public void testStringResource() throws IOException, URISyntaxException { - var r1 = new StringResource("hello, world"); + var r1 = StringResource.of("hello, world"); assertThat(r1.charSource().read()).isEqualTo("hello, world"); - var r2 = new StringResource("# Models\n"); + var r2 = StringResource.of("# Models\n"); assertThat(r2.charSource().read()).isEqualTo("# Models\n"); // NB: new StringResource("") is not supported, because // URI.create("string:") causes an java.net.URISyntaxException. + var r3 = StringResource.of(""); + assertThat(r3.charSource().read()).isEmpty(); } } diff --git a/common/protobuf/src/main/java/dev/enola/common/protobuf/Anys.java b/common/protobuf/src/main/java/dev/enola/common/protobuf/Anys.java deleted file mode 100644 index 2f123a3bd..000000000 --- a/common/protobuf/src/main/java/dev/enola/common/protobuf/Anys.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * Copyright 2024 The Enola Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dev.enola.common.protobuf; - -import com.google.protobuf.Any; -import com.google.protobuf.Descriptors.Descriptor; -import com.google.protobuf.DynamicMessage; -import com.google.protobuf.GeneratedMessageV3; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.Message; - -/** Utility functions related to {@link Any}. */ -public class Anys { - - private final DescriptorProvider descriptorProvider; - - public Anys(DescriptorProvider descriptorProvider) { - this.descriptorProvider = descriptorProvider; - } - - public DynamicMessage toMessage(Any any) throws InvalidProtocolBufferException { - var typeURL = any.getTypeUrl(); - Descriptor descriptor = descriptorProvider.getDescriptorForTypeUrl(typeURL); - return DynamicMessage.parseFrom(descriptor, any.getValue()); - } - - @SuppressWarnings("unchecked") - public static T dynamicToStaticMessage( - Message message, GeneratedMessageV3.Builder builder) { - if (message instanceof GeneratedMessageV3) { - return (T) message; - } - - var bytes = message.toByteArray(); - try { - return (T) builder.mergeFrom(bytes).buildPartial(); - } catch (IllegalArgumentException | SecurityException | InvalidProtocolBufferException e) { - throw new IllegalArgumentException("No good: " /*+ klass*/, e); - } - } -} diff --git a/common/protobuf/src/main/java/dev/enola/common/protobuf/DescriptorProvider.java b/common/protobuf/src/main/java/dev/enola/common/protobuf/DescriptorProvider.java index dc612e729..62000b434 100644 --- a/common/protobuf/src/main/java/dev/enola/common/protobuf/DescriptorProvider.java +++ b/common/protobuf/src/main/java/dev/enola/common/protobuf/DescriptorProvider.java @@ -20,5 +20,9 @@ import com.google.protobuf.Descriptors.Descriptor; public interface DescriptorProvider { + + // TODO Rename to findByTypeUrl() for consistency Descriptor getDescriptorForTypeUrl(String messageTypeURL); + + Descriptor findByName(String name); } diff --git a/common/protobuf/src/main/java/dev/enola/common/protobuf/TypeRegistryDescriptorProvider.java b/common/protobuf/src/main/java/dev/enola/common/protobuf/TypeRegistryDescriptorProvider.java index ea20ffc77..d51999bb6 100644 --- a/common/protobuf/src/main/java/dev/enola/common/protobuf/TypeRegistryDescriptorProvider.java +++ b/common/protobuf/src/main/java/dev/enola/common/protobuf/TypeRegistryDescriptorProvider.java @@ -37,4 +37,9 @@ public Descriptor getDescriptorForTypeUrl(String messageTypeURL) { throw new IllegalArgumentException(messageTypeURL, e); } } + + @Override + public Descriptor findByName(String name) { + return typeRegistry.find(name); + } } diff --git a/core/impl/src/main/java/dev/enola/core/EnolaServiceProvider.java b/core/impl/src/main/java/dev/enola/core/EnolaServiceProvider.java index fcc020551..7edc550bf 100644 --- a/core/impl/src/main/java/dev/enola/core/EnolaServiceProvider.java +++ b/core/impl/src/main/java/dev/enola/core/EnolaServiceProvider.java @@ -18,7 +18,10 @@ package dev.enola.core; import com.google.common.collect.ImmutableList; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.ExtensionRegistry; +import dev.enola.common.protobuf.DescriptorProvider; import dev.enola.common.protobuf.ValidationException; import dev.enola.core.aspects.ErrorTestAspect; import dev.enola.core.aspects.FilestoreRepositoryAspect; @@ -30,19 +33,64 @@ import dev.enola.core.meta.EntityKindRepository; import dev.enola.core.meta.SchemaAspect; import dev.enola.core.meta.TypeRegistryWrapper; +import dev.enola.core.meta.TypeRegistryWrapper.Builder; +import dev.enola.core.meta.proto.Type; +import dev.enola.core.thing.ThingConnector; +import dev.enola.core.thing.ThingConnectorService; +import dev.enola.core.type.ProtoService; +import dev.enola.core.type.TypeRepositoryBuilder; +import dev.enola.core.view.EnolaMessages; import java.lang.reflect.InvocationTargetException; import java.nio.file.Path; public class EnolaServiceProvider { - private final EnolaServiceRegistry enolaService; private final TypeRegistryWrapper typeRegistry; + private final DescriptorProvider descriptorProvider = + new DescriptorProvider() { + + @Override + public Descriptor getDescriptorForTypeUrl(String messageTypeURL) { + return typeRegistry.getDescriptorForTypeUrl(messageTypeURL); + } + + @Override + public Descriptor findByName(String name) { + return typeRegistry.findByName(name); + } + }; + + private final EnolaServiceRegistry enolaService; + private final EnolaMessages enolaMessages; + @Deprecated // replace all usages with the new constructor (below) public EnolaServiceProvider(EntityKindRepository ekr) throws ValidationException, EnolaException { + this(ekr, new TypeRepositoryBuilder().build()); + } + + public EnolaServiceProvider(EntityKindRepository ekr, Repository tyr) + throws ValidationException, EnolaException { var esb = EnolaServiceRegistry.builder(); var trb = TypeRegistryWrapper.newBuilder(); + processBuiltIns(esb); + process(esb, ekr, trb); + process(esb, tyr, trb); + this.typeRegistry = trb.build(); + this.enolaService = esb.build(); + this.enolaMessages = new EnolaMessages(typeRegistry, ExtensionRegistry.getEmptyRegistry()); + } + + private void processBuiltIns(dev.enola.core.EnolaServiceRegistry.Builder esb) { + esb.register(ProtoService.TYPE().build(), new ProtoService(descriptorProvider)); + } + + private void process( + EnolaServiceRegistry.Builder esb, + EntityKindRepository ekr, + TypeRegistryWrapper.Builder trb) + throws ValidationException, EnolaException { for (var ek : ekr.list()) { var aspectsBuilder = ImmutableList.builder(); @@ -115,8 +163,70 @@ public EnolaServiceProvider(EntityKindRepository ekr) trb.add(aspect.getDescriptors()); } } - this.typeRegistry = trb.build(); - this.enolaService = esb.build(); + } + + private void process(EnolaServiceRegistry.Builder esb, Repository tyr, Builder trb) + throws EnolaException { + for (var type : tyr.list()) { + var aspectsBuilder = ImmutableList.builder(); + for (var connector : type.getConnectorsList()) { + switch (connector.getTypeCase()) { + case ERROR: + throw new IllegalArgumentException( + "Error Connector not supported for Types"); + + case JAVA_CLASS: + var className = connector.getJavaClass(); + try { + var clazz = Class.forName(className); + var object = clazz.getDeclaredConstructor().newInstance(); + ThingConnector aspect = (ThingConnector) object; + aspectsBuilder.add(aspect); + break; + + } catch (ClassNotFoundException + | NoSuchMethodException + | InstantiationException + | IllegalAccessException + | InvocationTargetException e) { + // TODO Full ValidationException instead of IllegalArgumentException + throw new IllegalArgumentException( + "Java Class Connector failure for EntityKind: " + + type.getName(), + e); + } + + // TODO JAVA_GUICE Registry lookup? + + case FS: + throw new IllegalArgumentException( + "TODO: Implement FS Connector for Types!"); + // var fs = connector.getFs(); + // TODO aspectsBuilder.add(new + // FilestoreRepositoryAspect(Path.of(fs.getPath()), fs.getFormat())); + // break; + + case GRPC: + throw new IllegalArgumentException( + "TODO: Implement gRPC Connector for Types!"); + // TODO aspectsBuilder.add(new GrpcAspect(c.getGrpc())); + // break; + + case TYPE_NOT_SET: + // TODO Full ValidationException instead of IllegalArgumentException + throw new IllegalArgumentException( + "Connector Type not set in Type: " + type.getName()); + } + } + + var aspects = aspectsBuilder.build(); + var s = new ThingConnectorService(type, aspects, enolaMessages); + esb.register(type, s); + + for (var aspect : aspects) { + trb.add(aspect.getDescriptors()); + } + } } public EnolaService getEnolaService() { diff --git a/core/impl/src/main/java/dev/enola/core/EnolaServiceRegistry.java b/core/impl/src/main/java/dev/enola/core/EnolaServiceRegistry.java index 6203ba99c..4509b87d6 100644 --- a/core/impl/src/main/java/dev/enola/core/EnolaServiceRegistry.java +++ b/core/impl/src/main/java/dev/enola/core/EnolaServiceRegistry.java @@ -18,6 +18,7 @@ package dev.enola.core; import dev.enola.core.iri.URITemplateMatcherChain; +import dev.enola.core.meta.proto.Type; import dev.enola.core.proto.GetThingRequest; import dev.enola.core.proto.GetThingResponse; import dev.enola.core.proto.ID; @@ -83,6 +84,10 @@ public Builder register(ID ekid, EnolaService service) { return this; } + public void register(Type type, EnolaService service) { + b.add(type.getUri(), service); + } + public EnolaServiceRegistry build() { return new EnolaServiceRegistry(b.build()); } diff --git a/core/impl/src/main/java/dev/enola/core/EntityAspect.java b/core/impl/src/main/java/dev/enola/core/EntityAspect.java index 8d0b10da0..aa4308fd9 100644 --- a/core/impl/src/main/java/dev/enola/core/EntityAspect.java +++ b/core/impl/src/main/java/dev/enola/core/EntityAspect.java @@ -27,9 +27,11 @@ import java.util.List; /** - * API for in-process "connectors". This is the internal equivalent of the gRPC ConnectorService. + * API for in-process Entity "connectors". This is the internal equivalent of the gRPC + * ConnectorService. */ public interface EntityAspect { + // TODO Move (refactor) this into a package dev.enola.core.entity. void augment(Entity.Builder entity, EntityKind entityKind) throws EnolaException; diff --git a/core/impl/src/main/java/dev/enola/core/EntityAspectRepeater.java b/core/impl/src/main/java/dev/enola/core/EntityAspectRepeater.java index e4249b93f..747105630 100644 --- a/core/impl/src/main/java/dev/enola/core/EntityAspectRepeater.java +++ b/core/impl/src/main/java/dev/enola/core/EntityAspectRepeater.java @@ -29,6 +29,8 @@ * List)} implementation. */ public interface EntityAspectRepeater extends EntityAspect { + // TODO Move (refactor) this into a package dev.enola.core.entity. + @Override default void list( ConnectorServiceListRequest request, diff --git a/core/impl/src/main/java/dev/enola/core/EntityAspectService.java b/core/impl/src/main/java/dev/enola/core/EntityAspectService.java index 4ff5734cf..929664450 100644 --- a/core/impl/src/main/java/dev/enola/core/EntityAspectService.java +++ b/core/impl/src/main/java/dev/enola/core/EntityAspectService.java @@ -30,6 +30,7 @@ import java.util.ArrayList; +// TODO Move (refactor) this into a package dev.enola.core.entity. class EntityAspectService implements EnolaService { private final EntityKind entityKind; diff --git a/core/impl/src/main/java/dev/enola/core/Repository.java b/core/impl/src/main/java/dev/enola/core/Repository.java new file mode 100644 index 000000000..e4ed001ff --- /dev/null +++ b/core/impl/src/main/java/dev/enola/core/Repository.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.enola.core; + +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; + +public interface Repository { + // TODO Consider moving from package + // dev.enola.core to dev.enola.common.io.resource + + ImmutableCollection list(); + + ImmutableSet names(); + + T getByName(String name); +} diff --git a/core/impl/src/main/java/dev/enola/core/RepositoryBuilder.java b/core/impl/src/main/java/dev/enola/core/RepositoryBuilder.java new file mode 100644 index 000000000..6ff695c58 --- /dev/null +++ b/core/impl/src/main/java/dev/enola/core/RepositoryBuilder.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.enola.core; + +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedMap; + +public class RepositoryBuilder { + + private final ImmutableSortedMap.Builder items = ImmutableSortedMap.naturalOrder(); + + protected void add(String name, T item) { + items.put(name, item); + } + + public Repository build() { + return new RepositoryImpl(items.build()); + } + + protected O require(O what, String identification) { + if (what == null) throw new IllegalArgumentException("Missing required: " + identification); + if (what instanceof String) { + String whatString = (String) what; + if (whatString.trim().isEmpty()) + throw new IllegalArgumentException("Empty: " + identification); + } + return what; + } + + private class RepositoryImpl implements Repository { + private final ImmutableSortedMap items; + + private RepositoryImpl(ImmutableSortedMap items) { + this.items = items; + } + + @Override + public ImmutableSet names() { + return items.keySet(); + } + + @Override + public T getByName(String name) { + return items.get(name); + } + + @Override + public ImmutableCollection list() { + return items.values(); + } + } +} diff --git a/core/impl/src/main/java/dev/enola/core/aspects/ErrorTestAspect.java b/core/impl/src/main/java/dev/enola/core/aspects/ErrorTestAspect.java index 596a64a40..ccc6667ec 100644 --- a/core/impl/src/main/java/dev/enola/core/aspects/ErrorTestAspect.java +++ b/core/impl/src/main/java/dev/enola/core/aspects/ErrorTestAspect.java @@ -22,6 +22,8 @@ import dev.enola.core.meta.proto.EntityKind; import dev.enola.core.proto.Entity; +// TODO Rename (refactor) from ErrorTestAspect to ErrorEntityAspect +// TODO Move (refactor) this into a package dev.enola.core.entity.aspect public class ErrorTestAspect implements EntityAspectRepeater { private final String message; diff --git a/core/impl/src/main/java/dev/enola/core/aspects/FilestoreRepositoryAspect.java b/core/impl/src/main/java/dev/enola/core/aspects/FilestoreRepositoryAspect.java index 41626f31f..5dd49b433 100644 --- a/core/impl/src/main/java/dev/enola/core/aspects/FilestoreRepositoryAspect.java +++ b/core/impl/src/main/java/dev/enola/core/aspects/FilestoreRepositoryAspect.java @@ -42,6 +42,8 @@ import java.nio.file.Path; import java.util.List; +// TODO Rename (refactor) to FilestoreRepositoryEntityAspect +// TODO Move (refactor) this into a package dev.enola.core.entity.aspect public class FilestoreRepositoryAspect implements EntityAspect { private static final Logger LOG = LoggerFactory.getLogger(FilestoreRepositoryAspect.class); diff --git a/core/impl/src/main/java/dev/enola/core/aspects/GrpcAspect.java b/core/impl/src/main/java/dev/enola/core/aspects/GrpcAspect.java index 923e47821..d8aba3343 100644 --- a/core/impl/src/main/java/dev/enola/core/aspects/GrpcAspect.java +++ b/core/impl/src/main/java/dev/enola/core/aspects/GrpcAspect.java @@ -40,6 +40,9 @@ import java.util.ArrayList; import java.util.List; +// TODO Rename (refactor) to GrpcEntityAspect +// TODO Move (refactor) this into a package dev.enola.core.entity.aspect +// TODO Write a ThingGrpcAspect variant of this public class GrpcAspect implements Closeable, EntityAspect { private final ManagedChannel channel; diff --git a/core/impl/src/main/java/dev/enola/core/aspects/TimestampAspect.java b/core/impl/src/main/java/dev/enola/core/aspects/TimestampAspect.java index c667ca880..f84806039 100644 --- a/core/impl/src/main/java/dev/enola/core/aspects/TimestampAspect.java +++ b/core/impl/src/main/java/dev/enola/core/aspects/TimestampAspect.java @@ -26,6 +26,8 @@ import java.time.Clock; import java.time.Instant; +// TODO Rename (refactor) to TimestampEntityAspect +// TODO Move (refactor) this into a package dev.enola.core.entity.aspect public class TimestampAspect implements EntityAspectRepeater { private final Clock clock = Clock.systemUTC(); diff --git a/core/impl/src/main/java/dev/enola/core/aspects/UriTemplateAspect.java b/core/impl/src/main/java/dev/enola/core/aspects/UriTemplateAspect.java index eab12a704..983c2e449 100644 --- a/core/impl/src/main/java/dev/enola/core/aspects/UriTemplateAspect.java +++ b/core/impl/src/main/java/dev/enola/core/aspects/UriTemplateAspect.java @@ -39,6 +39,8 @@ import java.util.List; import java.util.Map; +// TODO Rename (refactor) to UriTemplateEntityAspect +// TODO Move (refactor) this into a package dev.enola.core.entity.aspect public class UriTemplateAspect implements EntityAspectRepeater { // TODO Fail if an URI Template refers to an unknown variable/value (as-is it's just ignored) diff --git a/core/impl/src/main/java/dev/enola/core/aspects/ValidationAspect.java b/core/impl/src/main/java/dev/enola/core/aspects/ValidationAspect.java index 492e58b13..21ef90e1a 100644 --- a/core/impl/src/main/java/dev/enola/core/aspects/ValidationAspect.java +++ b/core/impl/src/main/java/dev/enola/core/aspects/ValidationAspect.java @@ -33,6 +33,8 @@ import java.util.Map; import java.util.Set; +// TODO Rename (refactor) to ValidationEntityAspect +// TODO Move (refactor) this into a package dev.enola.core.entity.aspect public class ValidationAspect implements EntityAspectRepeater, MessageValidator { diff --git a/core/impl/src/main/java/dev/enola/core/meta/SchemaAspect.java b/core/impl/src/main/java/dev/enola/core/meta/SchemaAspect.java index 1c2402ca9..aa72e0086 100644 --- a/core/impl/src/main/java/dev/enola/core/meta/SchemaAspect.java +++ b/core/impl/src/main/java/dev/enola/core/meta/SchemaAspect.java @@ -54,7 +54,7 @@ public void augment(Entity.Builder entity, EntityKind entityKind) throws EnolaEx } var name = id.getPaths(0); - var descriptor = esp.getTypeRegistryWrapper().find(name).toProto(); + var descriptor = esp.getTypeRegistryWrapper().findByName(name).toProto(); var any = pack(descriptor, "type.googleapis.com/"); entity.putData("proto", any); } diff --git a/core/impl/src/main/java/dev/enola/core/meta/TypeRegistryWrapper.java b/core/impl/src/main/java/dev/enola/core/meta/TypeRegistryWrapper.java index 278f92a9e..ab84d6c7b 100644 --- a/core/impl/src/main/java/dev/enola/core/meta/TypeRegistryWrapper.java +++ b/core/impl/src/main/java/dev/enola/core/meta/TypeRegistryWrapper.java @@ -18,7 +18,6 @@ package dev.enola.core.meta; import com.google.protobuf.DescriptorProtos.FileDescriptorSet; -import com.google.protobuf.Descriptors; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.DescriptorValidationException; import com.google.protobuf.Descriptors.FileDescriptor; @@ -31,6 +30,9 @@ import java.util.List; import java.util.Set; +// TODO Move this class to package dev.enola.common.protobuf +// (next to class TypeRegistryDescriptorProvider) +// // TODO Optimization: This should allow clients like CLI to fetch as Map of Protos! public class TypeRegistryWrapper implements DescriptorProvider { private final TypeRegistry originalTypeRegistry; @@ -77,7 +79,8 @@ public List names() { return names; } - public Descriptors.GenericDescriptor find(String name) { + @Override + public Descriptor findByName(String name) { var descriptor = get().find(name); if (descriptor == null) { throw new IllegalArgumentException("Proto unknown: " + name); @@ -87,7 +90,7 @@ public Descriptors.GenericDescriptor find(String name) { @Override public Descriptor getDescriptorForTypeUrl(String typeURL) { - return (Descriptor) find(getTypeName(typeURL)); + return (Descriptor) findByName(getTypeName(typeURL)); } // This method is copy/pasted from com.google.protobuf.TypeRegistry diff --git a/core/impl/src/main/java/dev/enola/core/thing/ThingConnector.java b/core/impl/src/main/java/dev/enola/core/thing/ThingConnector.java new file mode 100644 index 000000000..725ba13d8 --- /dev/null +++ b/core/impl/src/main/java/dev/enola/core/thing/ThingConnector.java @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.enola.core.thing; + +import com.google.protobuf.Descriptors; +import com.google.protobuf.MessageLite; + +import dev.enola.core.EnolaException; +import dev.enola.core.meta.proto.Type; + +import java.util.Collections; +import java.util.List; + +/** + * API for in-process Thing "connectors". + * + *

This is the internal equivalent of the gRPC ConnectorService. + */ +@SuppressWarnings("restriction") +public interface ThingConnector { + + void augment(MessageLite.Builder thing, Type type) throws EnolaException; + + default List getDescriptors() throws EnolaException { + return Collections.emptyList(); + } +} diff --git a/core/impl/src/main/java/dev/enola/core/thing/ThingConnectorService.java b/core/impl/src/main/java/dev/enola/core/thing/ThingConnectorService.java new file mode 100644 index 000000000..629f0a1ab --- /dev/null +++ b/core/impl/src/main/java/dev/enola/core/thing/ThingConnectorService.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2023-2024 The Enola Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.enola.core.thing; + +import static java.util.Objects.requireNonNull; + +import com.google.common.collect.ImmutableList; +import com.google.protobuf.Any; +import com.google.protobuf.Message.Builder; + +import dev.enola.core.EnolaException; +import dev.enola.core.EnolaService; +import dev.enola.core.meta.proto.Type; +import dev.enola.core.proto.GetThingRequest; +import dev.enola.core.proto.GetThingResponse; +import dev.enola.core.proto.ListEntitiesRequest; +import dev.enola.core.proto.ListEntitiesResponse; +import dev.enola.core.view.EnolaMessages; + +public class ThingConnectorService implements EnolaService { + + private final Type type; + private final ImmutableList registry; + private final EnolaMessages enolaMessages; + + public ThingConnectorService( + Type type, ImmutableList aspects, EnolaMessages enolaMessages) { + this.type = type; + this.registry = aspects; + this.enolaMessages = requireNonNull(enolaMessages); + } + + @Override + public GetThingResponse getThing(GetThingRequest r) throws EnolaException { + var eri = r.getEri(); + + Builder thing = enolaMessages.newBuilder(type.getProto()); + + for (var aspect : registry) { + aspect.augment(thing, type); + } + + var responseBuilder = GetThingResponse.newBuilder(); + responseBuilder.setThing(Any.pack(thing.build())); + return responseBuilder.build(); + } + + @Override + public ListEntitiesResponse listEntities(ListEntitiesRequest r) throws EnolaException { + // TODO This doesn't make any sense for Things, and needs to be removed in future clean-up! + return ListEntitiesResponse.newBuilder().build(); + } +} diff --git a/core/impl/src/main/java/dev/enola/core/type/ProtoService.java b/core/impl/src/main/java/dev/enola/core/type/ProtoService.java new file mode 100644 index 000000000..0a938c1dd --- /dev/null +++ b/core/impl/src/main/java/dev/enola/core/type/ProtoService.java @@ -0,0 +1,73 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.enola.core.type; + +import com.google.protobuf.Any; +import com.google.protobuf.DescriptorProtos.DescriptorProto; + +import dev.enola.common.protobuf.DescriptorProvider; +import dev.enola.core.EnolaException; +import dev.enola.core.EnolaService; +import dev.enola.core.meta.proto.Type; +import dev.enola.core.proto.GetThingRequest; +import dev.enola.core.proto.GetThingResponse; +import dev.enola.core.proto.ListEntitiesRequest; +import dev.enola.core.proto.ListEntitiesResponse; + +public class ProtoService implements EnolaService { + + public static Type.Builder TYPE() { + return Type.newBuilder() + .setEmoji("🕵🏾‍♀️") + .setName("type.enola.dev/proto") + .setUri("proto/{FQN}") + // "google.protobuf.DescriptorProto" + .setProto(DescriptorProto.getDescriptor().getFullName()); + // NOT .addConnectors(Connector.newBuilder().setJavaClass(ProtoConnector.class.getName())); + } + + private final DescriptorProvider descriptorProvider; + + public ProtoService(DescriptorProvider descriptorProvider) { + this.descriptorProvider = descriptorProvider; + } + + // public static class ProtoConnector implements ThingConnector { + // @Override + // public void augment(Builder thing, Type type) throws EnolaException { + // // TODO Auto-generated method stub + // throw new UnsupportedOperationException("Unimplemented method 'augment'"); + // } + // } + + @Override + public GetThingResponse getThing(GetThingRequest r) throws EnolaException { + // TODO It's pretty dumb to duplicate extracting the parameter from ERI again here... :-( + var fqn = r.getEri().substring("proto/".length()); + var descriptor = descriptorProvider.findByName(fqn); + + var response = GetThingResponse.newBuilder(); + response.setThing(Any.pack(descriptor.toProto())); + return response.build(); + } + + @Override + public ListEntitiesResponse listEntities(ListEntitiesRequest r) throws EnolaException { + throw new UnsupportedOperationException("Won't implement legacy method 'listEntities'"); + } +} diff --git a/core/impl/src/main/java/dev/enola/core/type/TypeRepositoryBuilder.java b/core/impl/src/main/java/dev/enola/core/type/TypeRepositoryBuilder.java new file mode 100644 index 000000000..9d9dde08f --- /dev/null +++ b/core/impl/src/main/java/dev/enola/core/type/TypeRepositoryBuilder.java @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.enola.core.type; + +import com.google.protobuf.TypeRegistry; + +import dev.enola.core.RepositoryBuilder; +import dev.enola.core.meta.proto.Type; + +/** + * Builds Repository of {@link Type}. + * + *

Not to be confused with and totally unrelated to {@link TypeRegistry}. + */ +public class TypeRepositoryBuilder extends RepositoryBuilder { + + public TypeRepositoryBuilder add(Type.Builder type) { + require(type.getUri(), "uri"); + // TODO setUrl(...), based on some sort of baseURL to the Web UI + var name = require(type.getName(), "name"); + add(name, type.build()); + return this; + } + + public TypeRepositoryBuilder addAllTypes(Iterable types) { + for (Type.Builder type : types) { + add(type); + } + return this; + } +} diff --git a/core/impl/src/test/java/dev/enola/core/meta/TypeRepositoryTest.java b/core/impl/src/test/java/dev/enola/core/meta/TypeRepositoryTest.java new file mode 100644 index 000000000..e1f6bb176 --- /dev/null +++ b/core/impl/src/test/java/dev/enola/core/meta/TypeRepositoryTest.java @@ -0,0 +1,71 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.enola.core.meta; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import dev.enola.common.io.resource.ClasspathResource; +import dev.enola.common.protobuf.ProtoIO; +import dev.enola.core.Repository; +import dev.enola.core.meta.proto.Type; +import dev.enola.core.meta.proto.Types; +import dev.enola.core.type.TypeRepositoryBuilder; + +import org.junit.Test; + +import java.io.IOException; + +public class TypeRepositoryTest { + + @Test + public void loadBaseYAML() throws IOException { + var types = Types.newBuilder(); + var resource = new ClasspathResource("test-types.yaml"); + new ProtoIO().read(resource, types, Types.class); + + var trb = new TypeRepositoryBuilder(); + trb.addAllTypes(types.getTypesBuilderList()); + Repository tyr = trb.build(); + assertThat(tyr.getByName("enola.dev/test1")).isNotNull(); + assertThat(tyr.list()).hasSize(2); + } + + @Test + public void noDuplicates() { + var trb = new TypeRepositoryBuilder(); + var type = Type.newBuilder().setName("enola.dev/testDupe").setUri("enola.dev/testDupe"); + trb.add(type); + assertThrows(IllegalArgumentException.class, () -> assertThat(trb.add(type).build())); + } + + @Test + public void noName() { + var trb = new TypeRepositoryBuilder(); + var type = Type.newBuilder(); + assertThrows(IllegalArgumentException.class, () -> assertThat(trb.add(type).build())); + } + + @Test + public void noURI() { + var trb = new TypeRepositoryBuilder(); + var type = Type.newBuilder().setName("enola.dev/testDupe"); + assertThrows(IllegalArgumentException.class, () -> assertThat(trb.add(type).build())); + } +} diff --git a/core/impl/src/test/resources/test-types.yaml b/core/impl/src/test/resources/test-types.yaml new file mode 100644 index 000000000..95792e412 --- /dev/null +++ b/core/impl/src/test/resources/test-types.yaml @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023-2024 The Enola Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Switch from YAML to MD (with YAML in "header") + +types: + - name: enola.dev/test1 + uri: enola.dev/test1/{PARAMETER} + emoji: 🔗 + labels: + en: URL (Test) + string: + + - name: enola.dev/test2 + uri: enola.dev/test2 diff --git a/core/lib/BUILD b/core/lib/BUILD index c868153a7..75144d384 100644 --- a/core/lib/BUILD +++ b/core/lib/BUILD @@ -35,10 +35,19 @@ load("@rules_proto_grpc//doc:defs.bzl", "doc_markdown_compile") # https://rules-proto-grpc.com/en/latest/example.html#step-3-write-a-build-file +proto_library( + name = "ext_proto", + srcs = ["src/main/java/dev/enola/core/enola_ext.proto"], + deps = [ + "@com_google_protobuf//:descriptor_proto", + ], +) + proto_library( name = "core_proto", srcs = ["src/main/java/dev/enola/core/enola_core.proto"], deps = [ + ":ext_proto", "@com_google_protobuf//:any_proto", "@com_google_protobuf//:descriptor_proto", "@com_google_protobuf//:struct_proto", @@ -58,7 +67,11 @@ proto_library( proto_library( name = "meta_proto", srcs = ["src/main/java/dev/enola/core/meta/enola_meta.proto"], - deps = [":core_proto"], + deps = [ + ":core_proto", + ":ext_proto", + "@com_google_protobuf//:empty_proto", + ], ) proto_library( @@ -92,6 +105,7 @@ java_proto_library( deps = [ "connector_proto", "core_proto", + "ext_proto", "meta_proto", "util_proto", ], @@ -135,6 +149,7 @@ doc_markdown_compile( protos = [ "connector_proto", "core_proto", + "ext_proto", "meta_proto", "util_proto", ], @@ -150,6 +165,7 @@ buf_proto_lint_test( protos = [ "connector_proto", "core_proto", + # "ext_proto", "meta_proto", "util_proto", ], @@ -197,11 +213,19 @@ java_library( "src/test/java/**/*Test.java", ])] +go_proto_library( + name = "ext_go_proto", + importpath = "dev/enola/core/ext", + protos = [":ext_proto"], + visibility = [], +) + go_proto_library( name = "core_go_proto", importpath = "dev/enola/core", protos = [":core_proto"], visibility = [], + deps = [":ext_go_proto"], ) go_proto_library( @@ -217,7 +241,10 @@ go_proto_library( importpath = "dev/enola/core/meta", protos = [":meta_proto"], visibility = [], - deps = [":core_go_proto"], + deps = [ + ":core_go_proto", + ":ext_go_proto", + ], ) go_proto_library( diff --git a/core/lib/src/main/java/dev/enola/core/enola_core.proto b/core/lib/src/main/java/dev/enola/core/enola_core.proto index ec4257599..2ed7fd7d3 100644 --- a/core/lib/src/main/java/dev/enola/core/enola_core.proto +++ b/core/lib/src/main/java/dev/enola/core/enola_core.proto @@ -18,6 +18,7 @@ syntax = "proto3"; package dev.enola.core; +import "core/lib/src/main/java/dev/enola/core/enola_ext.proto"; import "google/protobuf/any.proto"; import "google/protobuf/descriptor.proto"; import "google/protobuf/timestamp.proto"; @@ -33,7 +34,6 @@ option go_package = "dev/enola/core"; // https://github.com/capnproto/capnproto/blob/master/c%2B%2B/src/capnp/schema.capnp, // et al. message Thing { - // TODO URI for Linked Data... oneof kind { // NB: There are intentionally no other "basic types" than string; because // this is ultimately only intended for end-user viewing (so we convert @@ -41,7 +41,6 @@ message Thing { LinkedText text = 4; List list = 5; Struct struct = 6; - // TODO Map? } message LinkedText { string string = 1; diff --git a/core/lib/src/main/java/dev/enola/core/enola_ext.proto b/core/lib/src/main/java/dev/enola/core/enola_ext.proto new file mode 100644 index 000000000..f4579e8d0 --- /dev/null +++ b/core/lib/src/main/java/dev/enola/core/enola_ext.proto @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright 2023-2024 The Enola Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package dev.enola; + +import "google/protobuf/descriptor.proto"; + +option java_string_check_utf8 = true; +option java_package = "dev.enola.core.ext.proto"; +option java_multiple_files = true; +option go_package = "dev/enola/core/ext"; + +extend google.protobuf.FieldOptions { + string type = 2734; +} + +// TODO Register 2734 on +// https://github.com/protocolbuffers/protobuf/blob/master/docs/options.md diff --git a/core/lib/src/main/java/dev/enola/core/meta/enola_meta.proto b/core/lib/src/main/java/dev/enola/core/meta/enola_meta.proto index f37063c9b..48e2ec0ef 100644 --- a/core/lib/src/main/java/dev/enola/core/meta/enola_meta.proto +++ b/core/lib/src/main/java/dev/enola/core/meta/enola_meta.proto @@ -19,16 +19,155 @@ syntax = "proto3"; package dev.enola.core.meta; import "core/lib/src/main/java/dev/enola/core/enola_core.proto"; +import "core/lib/src/main/java/dev/enola/core/enola_ext.proto"; +import "google/protobuf/empty.proto"; option java_string_check_utf8 = true; option java_package = "dev.enola.core.meta.proto"; option java_multiple_files = true; option go_package = "dev/enola/core/meta"; -// message MetaModelPackage { -// Scheme, as-in ID.parts.scheme; in lower case and without any spaces. -// TODO Enable this, with validation that entities don't have it, and -// "complete" it on read string package = 1; +message Import { + // URLs from where to load additional referenced Types. + repeated string types = 1 [(dev.enola.type) = "enola.dev/url"]; + + // TODO repeated string protos = 2; + // TODO repeated string json_schemas = 2; +} + +// Types are a collection of enola:dev.enola.core.meta.Type. +// +// This is kind of like a +// https://en.wikipedia.org/wiki/Domain_of_discourse#Universe_of_discourse +// or the https://en.wikipedia.org/wiki/Universe_(mathematics). +message Types { + // TODO Implement, with @Test! Import import = 1; + repeated Type types = 7; +} + +message Type { + // Unique ID of this Type. + // For example, "d19974d6-0695-458d-bdd4-3ad89578db92". + // + // This "provides a relatively short yet unambiguous way to refer to a type", + // as "fully-qualified type names may be large and waste space when + // transmitted on the wire", and it "lets programmers change the symbolic name + // while keeping a fixed ID" (inspired by + // https://capnproto.org/language.html#unique-ids). + // + // It's recommended that this is set by the human author of the Type, + // but if it's not, it will be automatically generated by hashing the name. + // (This defeats the purpose of a "permanent" ID - but it's at least possible + // to set it later, if a name is ever changed.) + // + // TODO "Nicely render" this in the Web UI, using dev.enola.core.ByteSeq. + // TODO Do we *really* this, actually? + // bytes id = 1 [(dev.enola.type) = "enola.dev/id"]; + + // Short technical name of this Type. + // Must be unique within the environment this Enola instance operates. + // Publicly, using something that looks like an IRI/URI/URL is a simple way + // for uniqueness. For example, "your.org/something" (which is technically a + // relative URI, by chance). This string is NOT (necessarily) a valid URL; + // e.g. you (generally) cannot "http GET your.org/something". As a convenience + // for humans which type this into their web browser, your.org MAY set up a + // "redirector" which responds with a 30x to somewhere "interesting" for a + // human (not a machine), but that's just "nice", nothing more. Enola will + // never use it as anything else than simply a unique string. + string name = 2 [(dev.enola.type) = "enola.dev/gun"]; + + // URL of this Type. + // For example, "https://demo.enola.dev/type/enola.dev/Person". + // You *CAN* http GET this URL. An Enola server will return a HTML page or + // JSON or something. This is NOT set by the author of the Type, but at + // runtime. It is based on the name. + // TODO string url = 3 [(dev.enola.type) = "enola.dev/url"]; + + // URI Template of instances of this Type. + // For example, "hello/{message}" - where the parameter "message" refers to a + // property; so instances would be e.g. "hello/world" and "hello/planets". + string uri = 4 [(dev.enola.type) = "enola.dev/uri-template"]; + + // Properties of this Type. + // TODO This needs to be Reference to a Property, not a contained Property... + // TODO TBD! map properties = 5; + + // Human readable label of this type, may be several words, any case. + // This is a map where the key is an IETF [BCP + // 47](https://www.rfc-editor.org/info/bcp47) "language code" (like "en" or + // "de-CH") and the value is text in that language. These can easily be + // changed at any time without breaking anything in Enola. + map labels = 20 [(dev.enola.type) = "enola.dev/mls"]; + + // Documentation description (as URL; either absolute, or URL relative to the + // model's location - from where a UI will serve it). + // TODO Is this a [(dev.enola.type) = "enola.dev/url"]? Really?? + string doc = 21 [(dev.enola.type) = "enola.dev/url"]; + + // The Emoji shown as prefix to the label in UIs, if there is no logo. + // This is not necessarily unique. + string emoji = 22; + + // Logo (as URL; either absolute, or URL relative to the model's location - + // from where a UI will serve it). + string logo_url = 23 [(dev.enola.type) = "enola.dev/url"]; + + // TODO Replace with... Type reference?!? Whoa. + oneof schema { + // EntityKind. + // This is from the original Enola design, and may later be removed. + // TODO Should this really be embedded? Really? Probably better an ID as + // reference? Good test! + // string entity_kind = 10 [(dev.enola.type) = "enola.dev/EntityKind"]; + EntityKind legacy = 30; + + // Protocol Buffers Message. + // For example, "google.protobuf.Timestamp". + // TODO We need to be able to "annotate" (?) Proto descriptors for LD... + string proto = 31 [(dev.enola.type) = "enola.dev/proto"]; + + // A "simple type" that "extends" a string. + // This is useful to add "semantics" to strings; e.g. URL, Email, etc. + google.protobuf.Empty string = 32; + + // A "binary" type, with "content" that's an (unstructured) bytes sequence. + google.protobuf.Empty binary = 33; + } + + repeated Connector connectors = 40; + + string java = 50; + repeated string javas = 51; + + // TODO Verbs! +} + +message Property { + uint32 id = 1; + + // Type of Property, as Name of another Type. + // TODO Re-consider this... this is wrong, because Property is Types sharable. + // string type = 2 [(dev.enola.type) = "enola.dev/type"]; + + // Link to something related. + // Intended both for human consumption in a UI, as well as machine readable + // linked data relationships. Typically a Template of an (HTML a/href-like) + // HTTP URL, or enola: URI. Template parameters refer to other properties of + // the Type. + string link = 10 [(dev.enola.type) = "enola.dev/uri-template"]; + + // Human readable label of this property, may be several words, any case. + // This is a map where the key is an IETF [BCP + // 47](https://www.rfc-editor.org/info/bcp47) "language code" (like "en" or + // "de-CH") and the value is text in that language. These can easily be + // changed at any time without breaking anything in Enola. + map labels = 20 [(dev.enola.type) = "enola.dev/mls"]; + + // Documentation description (as URL; either absolute, or URL relative to the + // model's location - from where a UI will serve it). + // TODO Is this a [(dev.enola.type) = "enola.dev/url"]? Really?? + string doc = 21 [(dev.enola.type) = "enola.dev/url"]; +} message EntityKinds { repeated EntityKind kinds = 1; @@ -146,6 +285,7 @@ message Data { message Connector { oneof type { // Always fails with this error message (for testing, only). + // TODO Remove this again? This was really useful in earlier testing, only. string error = 1; // Java class name for in-process connector on the Java classpath. diff --git a/core/lib/src/main/java/dev/enola/core/view/Things.java b/core/lib/src/main/java/dev/enola/core/view/Things.java index 2926f5bca..35e0986aa 100644 --- a/core/lib/src/main/java/dev/enola/core/view/Things.java +++ b/core/lib/src/main/java/dev/enola/core/view/Things.java @@ -88,7 +88,7 @@ private static Thing.Builder toThing(Object object, FieldDescriptor field, Messa private static Thing.Builder toThing(ID id) { var path = IDs.toPath(id); - return toThing(path, "enola:entity/" + path); + return toThing(path, "enola:" + path); } @VisibleForTesting diff --git a/docs/concepts/core.md b/docs/concepts/core.md index ff409b361..d6f470a0e 100644 --- a/docs/concepts/core.md +++ b/docs/concepts/core.md @@ -35,11 +35,15 @@ It _models_ real world concepts as what it terms _Entities,_ [identified by URI] across its Entities, as well as to arbitrary non-Enola URIs. (This notably includes traditional URLs like HTTP links, which models can use to create hyperlinks to UIs of applications managing Entities.) -Enola has [built-in interchangeable support](../use/rosetta/index.md) for JSON, YAML, and Text & Binary Protocol Buffers +## Formats + +Enola has [built-in interchangeable support](../use/rosetta/index.md) for JSON, YAML, and Text proto & Binary Protocol Buffers [wire formats](https://en.m.wikipedia.org/wiki/Comparison_of_data-serialization_formats) for entities. +## Schemas + Enola currently uses [Proto 3](https://protobuf.dev/programming-guides/proto3/) as its -Schema language, but is conceptually open to supporting other kinds of schemas in the future; perhaps e.g. -[JSON Schema](https://github.com/enola-dev/enola/issues/313), or XSD XML Schema, or +Schema language. It is conceptually open to supporting other kinds of schemas in the future; perhaps e.g. +[JSON Schema](https://github.com/enola-dev/enola/issues/313), or [Cap’n Proto](https://capnproto.org/language.html), or [TypeScript](https://www.typescriptlang.org/docs/handbook/2/objects.html) (à la [Typson](https://github.com/lbovet/typson)), or [XML Schema](https://en.wikipedia.org/wiki/XML_Schema_(W3C)) (XSD), or [YANG](https://en.wikipedia.org/wiki/YANG) or [FHIR](https://www.hl7.org/fhir/) or [Varlink](https://varlink.org/Interface-Definition) or [Web IDL](https://webidl.spec.whatwg.org) or [ASN.1](https://en.m.wikipedia.org/wiki/ASN.1) or [GNU poke](https://www.gnu.org/software/poke/). diff --git a/docs/concepts/uri.md b/docs/concepts/uri.md index a4c0f9805..5f87f5df6 100644 --- a/docs/concepts/uri.md +++ b/docs/concepts/uri.md @@ -76,6 +76,6 @@ could include other protocols to connect to another Enola server, for federation ## Internationalization The project envisions to eventually fully support -[_Internationalized Resource Identifiers_](https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier) -(URI), instead of ASCII only URI syntax; more testing to identify and fix any +_[Internationalized Resource Identifiers](https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier)_ +(IRIs), instead of only ASCII URI syntax; more testing to identify and fix any related gaps is required. diff --git a/docs/dev/setup.md b/docs/dev/setup.md index 7d47b9c76..82017fc3a 100644 --- a/docs/dev/setup.md +++ b/docs/dev/setup.md @@ -59,21 +59,24 @@ If you do still want to try, here's how to manually install what the development There are different Java (like Linux) "distributions" (all based on OpenJDK). The easiest way to install one of them is typically to use your OS' package manager: - sudo apt-get install openjdk-21-jdk openjdk-21-doc openjdk-21-source + sudo apt-get install openjdk-21-jdk openjdk-21-doc openjdk-21-source An alternative is to use e.g. [the SDKMAN!](https://sdkman.io) If you work on several projects using different Java versions, - then we recommend [using the great jEnv](https://www.jenv.be). + then we recommend using something like + [jEnv (with `.java-version`)](https://www.jenv.be), or + [asdf (with `.tool-versions`)](https://asdf-vm.com), or + [direnv (with `.envrc`)](https://direnv.net). 1. Install C/C++ etc. (it's required by the [Proto rule for Bazel](https://github.com/bazelbuild/rules_proto)), e.g. do: - sudo apt-get install build-essential + sudo apt-get install build-essential 1. Install [Python venv](https://docs.python.org/3/library/venv.html) (it's used by the presubmit and docs site generation), e.g. with: - sudo apt-get install python3-venv + sudo apt-get install python3-venv 1. Install [Bazelisk](https://github.com/bazelbuild/bazelisk) (NOT Bazel), on a (recent enough...) Debian/Ubuntu [with Go](https://go.dev/doc/install) diff --git a/docs/use/library/model.yaml b/docs/use/library/model.yaml index 79cbe9bcf..db8d36953 100644 --- a/docs/use/library/model.yaml +++ b/docs/use/library/model.yaml @@ -5,7 +5,7 @@ kinds: link: google: label: Google Book Search - uriTemplate: "https://www.google.com/search?tbm=bks&q={path.isbn}" + uriTemplate: "https://www.google.com/search?tbm=bks&q=isbn:{path.isbn}" - id: { ns: demo, entity: library, paths: [id] } label: Library diff --git a/docs/use/rosetta/index.md b/docs/use/rosetta/index.md index ac2a5066c..6be433807 100644 --- a/docs/use/rosetta/index.md +++ b/docs/use/rosetta/index.md @@ -22,7 +22,11 @@ Rosetta, inspired by [the Rosetta Stone](https://en.wikipedia.org/wiki/Rosetta_Stone), transforms between [`YAML`](https://yaml.org) ⇔ [`JSON`](https://www.json.org) ⇔ [`TextProto`](https://protobuf.dev/reference/protobuf/textformat-spec/) ⇔ -_[Binary Protocol Buffer "Wire"](https://protobuf.dev/programming-guides/encoding/)_ formats: +_[Binary Protocol Buffer "Wire"](https://protobuf.dev/programming-guides/encoding/)_ formats. + +Specifying the `--schema` flag is optional for YAML <=> JSON conversion, but required for TextProto. + +For example: ```bash cd .././.././.. $ ./enola rosetta --in=file:docs/use/library/model.yaml --out=file:docs/use/library/model.json --schema=EntityKinds