Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(core): Initial GET on CLI #104

Merged
merged 1 commit into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions ToDo.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
1. `MarkdownGenerator.java` add Mermaid (like in `demo-model-docgen.md`)
1. `MarkdownGenerator.java` https://github.com/google/closure-templates/issues/1300 ?

1. Enforce https://www.conventionalcommits.org like git commit messages
1. https://github.com/enola-dev/enola/issues/102 :
Enforce https://www.conventionalcommits.org like git commit messages
starting with feat/model/fix/build/docs/clean/format/refactor: and core/k8s/tools:
using https://github.com/jorisroovers/gitlint

Expand Down Expand Up @@ -82,11 +83,18 @@

1. [Run `mkdocs build` instead of in `build.bash` as a `sh_test` in Bazel with `docs/**` + `mkdocs.yaml` as (only) `srcs`](https://github.com/enola-dev/enola/compare/main...vorburger:enola:mkdocs_build_test) - fix weird problems

1. Proto design: Is this a real requirement, or can we forget about this: _"Note that IDs are not "unique", and 2 different IDs may refer to the same underlying object; for example: `k8s:pod?name=echoserver-6dfb6c7764-45gvk&...` and `k8s:pod?uid=561f1bec-f768-4c5b-b96e-37306d7f2f8a&...`"_
1. Proto design: Is this a real requirement, or can we forget about this: _"Note that IDs are not "unique", and 2
different IDs may refer to the same underlying object; for example: `k8s:pod?name=echoserver-6dfb6c7764-45gvk&...`
and `k8s:pod?uid=561f1bec-f768-4c5b-b96e-37306d7f2f8a&...`"_

1. Proto design: Should `EntityKind` have a _"param_parent, to allow grouping common ones, just during declaration in textproto, but inlined for use."_ or shall we forget about that?
1. Proto design: Should `EntityKind` have a _"param_parent, to allow grouping common ones, just during declaration in
textproto, but inlined for use."_ or shall we forget about that?

1. Proto design: Should we permit RPC clients to _"specify the ID in either (oneof)string text or "broken down" parts form. This is simply for dev convenience in UX such as CLI or Web UIs, and to avoid the proliferation of incompatible parsers. The implementation validates the text, and rejects e.g. "demo:foo?bad=a=b" or "demo:foo?bad=a&bad=b". The string text oneof form is NOT "decoded" like un-escaped (incl. un-quoted) at all, simply "split" ... ? There was `IDsTest.java` and `IDs.java` code related to this which I removed on 2023-03-19:_
1. Proto design: Should we permit RPC clients to _"specify the ID in either (oneof)string text or "broken down" parts
form. This is simply for dev convenience in UX such as CLI or Web UIs, and to avoid the proliferation of incompatible
parsers. The implementation validates the text, and rejects e.g. "demo:foo?bad=a=b" or "demo:foo?bad=a&bad=b". The
string text oneof form is NOT "decoded" like un-escaped (incl. un-quoted) at all, simply "split" ... ? There
was `IDsTest.java` and `IDs.java` code related to this which I removed on 2023-03-19:_

message ID {
oneof oneof {
Expand Down
2 changes: 2 additions & 0 deletions cli/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ java_binary(
main_class = "dev.enola.cli.Enola",
deps = [
"//common/common",
"//core/lib:core_proto_java_library",
"//core/lib:lib_java",
"//core/impl",
"@maven//:com_google_guava_guava",
"@maven//:info_picocli_picocli",
Expand Down
48 changes: 48 additions & 0 deletions cli/src/main/java/dev/enola/cli/CommandWithEntityID.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2023 The Enola <https://enola.dev> 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.cli;

import dev.enola.core.EnolaService;
import dev.enola.core.EnolaServiceProvider;
import dev.enola.core.IDs;
import dev.enola.core.meta.EntityKindRepository;
import dev.enola.core.proto.ID;

import picocli.CommandLine;

public abstract class CommandWithEntityID extends CommandWithModel {

// TODO @Parameters(index = "0..*") List<String> ids;
// TODO ID instead of String, with https://picocli.info/#_strongly_typed_everything
@CommandLine.Parameters(index = "0", paramLabel = "id", description = "ID of Entity")
String idString;

@Override
protected final void run(EntityKindRepository ekr) throws Exception {
// TODO Move elsewhere for continuous ("shell") mode, as this is "expensive".
EnolaService service = new EnolaServiceProvider().get(ekr);

ID id = IDs.parse(idString); // TODO replace with ITypeConverter
// TODO Validate id; here it must have ns+name+path!

run(ekr, service, id);
}

protected abstract void run(EntityKindRepository ekr, EnolaService service, ID id)
throws Exception;
}
12 changes: 10 additions & 2 deletions cli/src/main/java/dev/enola/cli/CommandWithModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,28 @@
import dev.enola.common.io.resource.ResourceProviders;
import dev.enola.core.meta.EntityKindRepository;

import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.Spec;

import java.io.PrintWriter;
import java.net.URI;

public abstract class CommandWithModel implements CheckedRunnable {

@CommandLine.Option(
protected PrintWriter out;
@Spec CommandSpec spec;

@Option(
names = {"--model"},
required = true,
description = "URI to EntityKinds (e.g. file:model.yaml)")
private URI model;

@Override
public final void run() throws Exception {
out = spec.commandLine().getOut();

var modelResource = new ResourceProviders().getReadableResource(model);
var ekr = new EntityKindRepository();
ekr.load(modelResource);
Expand Down
5 changes: 4 additions & 1 deletion cli/src/main/java/dev/enola/cli/Enola.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
subcommands = {
HelpCommand.class, // TODO , Version.class
AutoComplete.GenerateCompletion.class,
DocGen.class
DocGen.class,
ListKinds.class,
List.class,
Get.class
})
public class Enola {

Expand Down
40 changes: 40 additions & 0 deletions cli/src/main/java/dev/enola/cli/Get.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2023 The Enola <https://enola.dev> 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.cli;

import dev.enola.core.EnolaService;
import dev.enola.core.meta.EntityKindRepository;
import dev.enola.core.proto.GetEntityRequest;
import dev.enola.core.proto.ID;

import picocli.CommandLine.Command;

@Command(name = "get", description = "Get Entity")
public class Get extends CommandWithEntityID {

@Override
protected void run(EntityKindRepository ekr, EnolaService service, ID id) throws Exception {
var request = GetEntityRequest.newBuilder().setId(id).build();
var response = service.getEntity(request);
var entity = response.getEntity();

// TODO Allow formatting it as JSON or YAML, via Resource!
// TODO What else do we do with the Entity?!
out.println(entity);
}
}
23 changes: 23 additions & 0 deletions cli/src/main/java/dev/enola/cli/List.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dev.enola.cli;

import dev.enola.core.EnolaService;
import dev.enola.core.IDs;
import dev.enola.core.meta.EntityKindRepository;
import dev.enola.core.proto.ID;

import picocli.CommandLine.Command;

@Command(name = "list", description = "List Entities")
public class List extends CommandWithEntityID {

// TODO Implement, after adding a list() method to EnolaService
// Similar to, but functionally separate from, the list() on EntityKindRepository.

// With EntityKind name asks connector to list (first N?) entity IDs
// With path asks connector, and behavior is connector specific; FileRepoConnector appends a *

@Override
protected void run(EntityKindRepository ekr, EnolaService service, ID id) throws Exception {
throw new IllegalArgumentException("TODO Implement support to list: " + IDs.toPath(id));
}
}
14 changes: 14 additions & 0 deletions cli/src/main/java/dev/enola/cli/ListKinds.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.enola.cli;

import dev.enola.core.IDs;
import dev.enola.core.meta.EntityKindRepository;

import picocli.CommandLine;

@CommandLine.Command(name = "list-kinds", description = "List known Entity Kinds")
public class ListKinds extends CommandWithModel {
@Override
protected void run(EntityKindRepository ekr) throws Exception {
ekr.listID().forEach(ekid -> out.println(IDs.toPath(ekid)));
}
}
4 changes: 2 additions & 2 deletions cli/src/main/java/dev/enola/cli/Version.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
*/
package dev.enola.cli;

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Spec;

@CommandLine.Command(name = "version", description = "Show CLI version (same as --version)")
@Command(name = "version", description = "Show CLI version (same as --version)")
public class Version implements CheckedRunnable {

// TODO For some reason this doesn't work...
Expand Down
28 changes: 28 additions & 0 deletions cli/src/test/java/dev/enola/cli/EnolaTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,32 @@ public void testDocGen() throws IOException {
Truth.assertThat(r.charSource().read()).endsWith(MarkdownDocGenerator.FOOTER);
}
}

@Test
public void testListKind() {
var exec = cli("-v", "list-kinds", "--model", "classpath:cli-test-model.textproto");
assertThat(exec).err().isEmpty();
assertThat(exec).hasExitCode(0).out().isEqualTo("test.foobar/name\n");
}

@Test
public void testGet() {
var exec =
cli(
"-v",
"get",
"--model",
"classpath:cli-test-model.textproto",
"test.foobar/helo");
assertThat(exec).err().isEmpty();
assertThat(exec)
.hasExitCode(0)
.out()
.isEqualTo(
"id {\n"
+ " ns: \"test\"\n"
+ " entity: \"foobar\"\n"
+ " paths: \"helo\"\n"
+ "}\n\n");
}
}
2 changes: 1 addition & 1 deletion cli/src/test/resources/cli-test-model.textproto
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@
# proto-message: EntityKinds

kinds {
id: { ns: "test" entity: "test" paths: "uuid" }
id: { ns: "test" entity: "foobar" paths: "name" }
emoji: "💂"
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.net.URI;

class EmptyResource implements ReadableResource {
// TODO Perhaps rename this to VoidResource with void:/ URI?

static final EmptyResource INSTANCE = new EmptyResource();

Expand Down
16 changes: 16 additions & 0 deletions core/impl/src/main/java/dev/enola/core/EnolaServiceProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package dev.enola.core;

import dev.enola.core.meta.EntityKindRepository;

public class EnolaServiceProvider {

public EnolaService get(EntityKindRepository ekr) {
var r = new EnolaServiceRegistry();
for (var ek : ekr.list()) {
var s = new EntityAspectService();

r.register(ek.getId(), s);
}
return r;
}
}
53 changes: 53 additions & 0 deletions core/impl/src/main/java/dev/enola/core/EnolaServiceRegistry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2023 The Enola <https://enola.dev> 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 dev.enola.core.proto.GetEntityRequest;
import dev.enola.core.proto.GetEntityResponse;
import dev.enola.core.proto.ID;

import java.util.HashMap;
import java.util.Map;

class EnolaServiceRegistry implements EnolaService {

private final Map<ID, EnolaService> registry;

public EnolaServiceRegistry() {
registry = new HashMap<>();
}

public void register(ID id, EnolaService service) {
var lookup = IDs.withoutPath(id);
var existing = registry.get(lookup);
if (existing != null) {
throw new IllegalArgumentException("Service already registered for: " + lookup);
}
registry.put(lookup, service);
}

@Override
public GetEntityResponse getEntity(GetEntityRequest r) {
var lookup = IDs.withoutPath(r.getId());
var delegate = registry.get(lookup);
if (delegate == null) {
throw new IllegalArgumentException("No Connector registered for: " + lookup);
}
return delegate.getEntity(r);
}
}
8 changes: 8 additions & 0 deletions core/impl/src/main/java/dev/enola/core/EntityAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package dev.enola.core;

import dev.enola.core.proto.Entity;

interface EntityAspect {

void augment(Entity.Builder entity);
}
30 changes: 30 additions & 0 deletions core/impl/src/main/java/dev/enola/core/EntityAspectService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package dev.enola.core;

import dev.enola.core.proto.Entity;
import dev.enola.core.proto.GetEntityRequest;
import dev.enola.core.proto.GetEntityResponse;

import java.util.ArrayList;
import java.util.List;

class EntityAspectService implements EnolaService {

private final List<EntityAspect> registry = new ArrayList<>();

public void add(EntityAspect aspect) {
registry.add(aspect);
}

@Override
public GetEntityResponse getEntity(GetEntityRequest r) {
var entity = Entity.newBuilder();
entity.setId(r.getId());

for (var aspect : registry) {
aspect.augment(entity);
}

var response = GetEntityResponse.newBuilder().setEntity(entity).build();
return response;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public void render(EntityKindRepository kinds, Appendable md) throws IOException
}

private void render(EntityKind ek, Appendable md) throws IOException {
ID idWithoutPathArguments = ID.newBuilder(ek.getId()).clearPaths().build();
ID idWithoutPathArguments = IDs.withoutPath(ek.getId());
String fqn = IDs.toPath(idWithoutPathArguments);
List<String> pathArguments =
ek.getId().getPathsList().stream().collect(Collectors.toList());
Expand Down
Loading