From 6b5e5a919acd32bfff1277473475679f2de6ebb9 Mon Sep 17 00:00:00 2001 From: Chris K Wensel Date: Thu, 3 Aug 2023 15:03:57 -0700 Subject: [PATCH] include generated documentation via the cli and integration with antora --- antora.yml | 10 + ...terless.java-common-conventions.gradle.kts | 9 +- clusterless-main-common/build.gradle.kts | 3 +- .../java/clusterless/printer/Printer.java | 51 ++++- clusterless-main/build.gradle.kts | 60 ++++++ clusterless-main/src/main/antora/antora.yml | 6 + .../main/antora/modules/ROOT/pages/index.adoc | 27 +++ .../main/java/clusterless/ShowCommand.java | 6 +- .../main/java/clusterless/ShowComponents.java | 199 ++++++++++-------- .../resources/templates/components-adoc.hbs | 20 ++ .../resources/templates/components-cli.hbs | 11 + .../templates/components-list-adoc.hbs | 4 + .../components-list-partial-adoc.hbs | 3 + 13 files changed, 317 insertions(+), 92 deletions(-) create mode 100644 antora.yml create mode 100644 clusterless-main/src/main/antora/antora.yml create mode 100644 clusterless-main/src/main/antora/modules/ROOT/pages/index.adoc create mode 100644 clusterless-main/src/main/resources/templates/components-adoc.hbs create mode 100644 clusterless-main/src/main/resources/templates/components-cli.hbs create mode 100644 clusterless-main/src/main/resources/templates/components-list-adoc.hbs create mode 100644 clusterless-main/src/main/resources/templates/components-list-partial-adoc.hbs diff --git a/antora.yml b/antora.yml new file mode 100644 index 00000000..6f5ccf9c --- /dev/null +++ b/antora.yml @@ -0,0 +1,10 @@ +name: clusterless +title: Clusterless Reference +version: wip-1.0 +ext: + collector: + run: + command: ./gradlew generateDocs -i + scan: + dir: clusterless-main/build/docs + diff --git a/build-logic/src/main/kotlin/clusterless.java-common-conventions.gradle.kts b/build-logic/src/main/kotlin/clusterless.java-common-conventions.gradle.kts index 82e76a8e..ae68e0f3 100644 --- a/build-logic/src/main/kotlin/clusterless.java-common-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/clusterless.java-common-conventions.gradle.kts @@ -19,10 +19,17 @@ val versionProperties = Properties().apply { load(FileInputStream(File(rootProject.rootDir, "version.properties"))) } -val release = false; +val release = false; // test if release branch val buildNumber = System.getenv("GITHUB_RUN_NUMBER") ?: "dev" val wipReleases = "wip-${buildNumber}" + +val versionLabel = if (release) + "${versionProperties["clusterless.release.major"]}-${versionProperties["clusterless.release.minor"]}" +else "${versionProperties["clusterless.release.major"]}-wip" + +ext.set("versionLabel", versionLabel) + version = if (release) "${versionProperties["clusterless.release.major"]}-${versionProperties["clusterless.release.minor"]}" else "${versionProperties["clusterless.release.major"]}-${wipReleases}" diff --git a/clusterless-main-common/build.gradle.kts b/clusterless-main-common/build.gradle.kts index c72a93a1..4d2bb349 100644 --- a/clusterless-main-common/build.gradle.kts +++ b/clusterless-main-common/build.gradle.kts @@ -15,4 +15,5 @@ dependencies { implementation(project(":clusterless-model")) implementation("info.picocli:picocli") -} \ No newline at end of file + implementation("com.github.jknack:handlebars") +} diff --git a/clusterless-main-common/src/main/java/clusterless/printer/Printer.java b/clusterless-main-common/src/main/java/clusterless/printer/Printer.java index a026854f..7257d2d6 100644 --- a/clusterless-main-common/src/main/java/clusterless/printer/Printer.java +++ b/clusterless-main-common/src/main/java/clusterless/printer/Printer.java @@ -8,19 +8,38 @@ package clusterless.printer; +import com.github.jknack.handlebars.Context; +import com.github.jknack.handlebars.Handlebars; +import com.github.jknack.handlebars.Options; +import com.github.jknack.handlebars.Template; +import com.github.jknack.handlebars.context.MapValueResolver; +import com.github.jknack.handlebars.helper.StringHelpers; import picocli.CommandLine; -import java.io.OutputStreamWriter; -import java.io.PrintStream; -import java.io.Writer; +import java.io.*; import java.util.Collection; +import java.util.Map; + +import static com.github.jknack.handlebars.internal.lang3.Validate.notNull; /** * */ public class Printer { - @CommandLine.Option(names = {"-j", "--json"}, description = "print results as json") + @CommandLine.Option( + names = {"-j", "--json"}, + description = "print results as json", + hidden = true + ) boolean json = false; + private static final Handlebars handlebars = new Handlebars() + .prettyPrint(false); + + static { + StringHelpers.register(handlebars); + handlebars.registerHelper("indent", Printer::indent); + } + private PrintStream out = System.out; public Printer() { @@ -37,4 +56,28 @@ public void println(String string) { public Writer writer() { return new OutputStreamWriter(out); } + + public void writeWithTemplate(String template, Map params, Writer writer) { + try { + Context context = Context + .newBuilder(params) + .resolver( + MapValueResolver.INSTANCE + ) + .build(); + + Template compile = handlebars.compile(template); + + compile.apply(context, writer); + writer.flush(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + protected static CharSequence indent(final Object value, final Options options) { + Integer width = options.param(0, 4); + notNull(width, "found 'null', expected 'indent'"); + return value.toString().trim().indent(width); + } } diff --git a/clusterless-main/build.gradle.kts b/clusterless-main/build.gradle.kts index 87695086..a51a561b 100644 --- a/clusterless-main/build.gradle.kts +++ b/clusterless-main/build.gradle.kts @@ -96,3 +96,63 @@ tasks.register("release") { dependsOn("distZip") dependsOn("jreleaserRelease") } + +tasks.register("generateComponentDocs") { + dependsOn("installDist") + + workingDir = file("build/install/clusterless/bin") + commandLine = listOf( + "cls", + "show", + "component", + "--describe-all", + "--output", + "${buildDir}/generated-docs/modules/components" + ) +} + +tasks.register("generateComponentIndex") { + dependsOn("installDist") + + workingDir = file("build/install/clusterless/bin") + commandLine = listOf( + "cls", + "show", + "component", + "--list", + "--output", + "${buildDir}/generated-docs/modules/components/" + ) +} + +tasks.register("generateComponentPartial") { + dependsOn("installDist") + + workingDir = file("build/install/clusterless/bin") + commandLine = listOf( + "cls", + "show", + "component", + "--list", + "--output", + "${buildDir}/generated-docs/modules/components/partials", + "--name", + "components.adoc", + "--template", + "components-list-partial-adoc" + ) +} + +tasks.register("generateDocs") { + dependsOn("generateComponentDocs") + dependsOn("generateComponentIndex") + dependsOn("generateComponentPartial") + + from("src/main/antora") { + filter { + it.replace("{{projectVersion}}", project.ext["versionLabel"].toString()) + } + } + from("${buildDir}/generated-docs/") + into("${buildDir}/docs/") +} diff --git a/clusterless-main/src/main/antora/antora.yml b/clusterless-main/src/main/antora/antora.yml new file mode 100644 index 00000000..28c8cd4a --- /dev/null +++ b/clusterless-main/src/main/antora/antora.yml @@ -0,0 +1,6 @@ +name: reference +title: Clusterless Reference +version: {{projectVersion}} +start_page: reference::index.adoc +nav: + - modules/components/nav.adoc diff --git a/clusterless-main/src/main/antora/modules/ROOT/pages/index.adoc b/clusterless-main/src/main/antora/modules/ROOT/pages/index.adoc new file mode 100644 index 00000000..f711974b --- /dev/null +++ b/clusterless-main/src/main/antora/modules/ROOT/pages/index.adoc @@ -0,0 +1,27 @@ += Clusterless Reference + +== Command Line + +The Clusterless application is a command line utility named `cls`. + +`cls --help` is the starting point for the usage documentation. + +== Providers + +Currently, only Amazon Web Services (AWS) is supported. + +== Components + +Components are either: + +* xref:guide:concepts:resource.adoc[Resources] +* xref:guide:concepts:boundary.adoc[Boundaries] +* xref:guide:concepts:arc.adoc[Arcs] + +All components: + +include::reference:components:partial$components.adoc[] + +== Models + +The Clusterless project file has a number of JSON objects or models that need to be provided in order to create a valid clusterless deployable project. diff --git a/clusterless-main/src/main/java/clusterless/ShowCommand.java b/clusterless-main/src/main/java/clusterless/ShowCommand.java index f3e839df..f6eaec9c 100644 --- a/clusterless-main/src/main/java/clusterless/ShowCommand.java +++ b/clusterless-main/src/main/java/clusterless/ShowCommand.java @@ -55,7 +55,7 @@ static class Exclusive { @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @CommandLine.Option( - names = "--template", + names = "--model", arity = "1", description = "print the json template of element" ) @@ -82,7 +82,7 @@ public Integer call() throws Exception { } else if (exclusive.list.isPresent() && exclusive.list.get()) { return handleList(); } else if (exclusive.template.isPresent()) { - return handleTemplte(); + return handleTemplate(); } else if (exclusive.component.isPresent()) { return handleDescribe(); } @@ -94,7 +94,7 @@ protected Integer handleList() throws Exception { return 0; } - protected Integer handleTemplte() throws Exception { + protected Integer handleTemplate() throws Exception { return 0; } diff --git a/clusterless-main/src/main/java/clusterless/ShowComponents.java b/clusterless-main/src/main/java/clusterless/ShowComponents.java index f0414c1f..5c660b77 100644 --- a/clusterless-main/src/main/java/clusterless/ShowComponents.java +++ b/clusterless-main/src/main/java/clusterless/ShowComponents.java @@ -15,41 +15,96 @@ import clusterless.managed.component.ProvidesComponent; import clusterless.model.Model; import clusterless.model.Struct; +import clusterless.naming.Label; import clusterless.substrate.SubstrateProvider; import clusterless.util.Annotations; -import com.github.jknack.handlebars.Context; -import com.github.jknack.handlebars.Handlebars; -import com.github.jknack.handlebars.Options; -import com.github.jknack.handlebars.Template; -import com.github.jknack.handlebars.context.MapValueResolver; -import com.github.jknack.handlebars.helper.StringHelpers; +import clusterless.util.ExitCodeException; +import org.jetbrains.annotations.NotNull; import picocli.CommandLine; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.io.Writer; +import java.io.*; import java.lang.reflect.InvocationTargetException; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.TreeSet; -import java.util.function.BiFunction; - -import static com.github.jknack.handlebars.internal.lang3.Validate.notNull; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; @CommandLine.Command( name = "component" ) public class ShowComponents extends ShowCommand.BaseShow { + interface Handler { + int handle(String name, ComponentService service, Class structClass); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + @CommandLine.Option( + names = "--output", + arity = "1", + description = "write out the documentation", + hidden = true + ) + Optional output; + + @CommandLine.Option( + names = "--template", + arity = "1", + description = "the documentation template to use", + hidden = true + ) + Optional template; + + @CommandLine.Option( + names = "--name", + arity = "1", + description = "the documentation file name to use", + hidden = true + ) + Optional name; + public ShowComponents() { } protected Integer handleList() { Map providers = showCommand.main.substratesOptions().requestedProvider(); + Set ordered = new TreeSet<>(); + for (Map.Entry entry : providers.entrySet()) { - showCommand.main.printer().println(entry.getValue().models().keySet()); + ordered.addAll(entry.getValue().models().keySet()); + } + + if (output.isPresent()) { + try { + Path path = Paths.get(output.get()); + path.toFile().mkdirs(); + + File file = path + .resolve(name.orElse("nav.adoc")) + .toFile(); + + Writer writer = new FileWriter(file); + + List> components = new ArrayList<>(); + + ordered.forEach(c -> components.add(Map.of( + "name", c, + "filename", createFileName(c) + ))); + + Map params = Map.of( + "title", "Clusterless Components", + "components", components + ); + + String partial = template.orElse("components-list-adoc"); + showCommand.main.printer().writeWithTemplate("/templates/" + partial, params, writer); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + showCommand.main.printer().println(ordered); } return 0; @@ -59,115 +114,93 @@ protected Integer handleList() { protected Integer handleDescribeAll() { Map providers = showCommand.main.substratesOptions().requestedProvider(); - boolean first = true; for (Map.Entry entry : providers.entrySet()) { Set names = new TreeSet<>(entry.getValue().models().keySet()); for (String component : names) { - first = delimit(first); - handle(this::printDescription, component); + handle(component, this::printDescription); } } return 0; } - protected Integer handleTemplte() { - return handle(this::printModel, exclusive.template.orElseThrow()); + protected Integer handleTemplate() { + return handle(exclusive.template.orElseThrow(), this::printModel); } @Override protected Integer handleDescribe() { - return handle(this::printDescription, exclusive.component.orElseThrow()); + return handle(exclusive.component.orElseThrow(), this::printDescription); } - private int handle(BiFunction, Class, Integer> func, String name) { + private int handle(String name, Handler func) { Map providers = showCommand.main.substratesOptions().requestedProvider(); for (Map.Entry entry : providers.entrySet()) { ComponentService componentService = entry.getValue().components().get(name); if (componentService != null) { - return func.apply(componentService, componentService.modelClass()); + return func.handle(name, componentService, componentService.modelClass()); } } - throw new IllegalArgumentException("no model found for: " + name); + throw new ExitCodeException("no model found for: " + name, 1); } - protected int printModel(ComponentService componentService, Class modelClass) { + protected int printModel(String name, ComponentService componentService, Class modelClass) { showCommand.main.printer().println(getModel(modelClass)); return 0; } - protected int printDescription(ComponentService componentService, Class modelClass) { - Class componentClass = componentService.getClass(); - Optional providesComponent = Annotations.find(componentClass, ProvidesComponent.class); - - if (providesComponent.isEmpty()) { - throw new IllegalStateException("component does not have a ProvidesComponent annotation: " + componentClass.getName()); - } - - String template = """ - Name: - {{name}} - - Synopsis: - {{{indent synopsis 4}}} - {{#description~}} - Description: - {{{indent description 4}}} - {{/description~}} - Template: - {{{model}}} - """; - - Map params = Map.of( - "name", providesComponent.get().type(), - "synopsis", providesComponent.get().synopsis(), - "description", providesComponent.get().description(), - "model", getModel(modelClass) - ); - - write(template, params); - return 0; - } - - private void write(String template, Map params) { + protected int printDescription(String name, ComponentService componentService, Class modelClass) { try { - Context context = Context - .newBuilder(params) - .resolver( - MapValueResolver.INSTANCE - ) - .build(); + Writer writer = showCommand.main.printer().writer(); + String template = "/templates/components-cli"; - Handlebars handlebars = new Handlebars() - .prettyPrint(false); + if (output.isPresent()) { + template = "/templates/components-adoc"; + Path path = Paths.get(output.get()).resolve("pages"); + path.toFile().mkdirs(); - StringHelpers.register(handlebars); + File file = path + .resolve(createFileName(name)) + .toFile(); - handlebars.registerHelper("indent", this::indent); + writer = new FileWriter(file); + } - Template compile = handlebars.compileInline(template); + printDescriptionUsing(componentService, modelClass, template, writer); - Writer writer = showCommand.main.printer().writer(); - compile.apply(context, writer); - writer.flush(); + writer.close(); } catch (IOException e) { throw new UncheckedIOException(e); } + + return 0; } - protected CharSequence indent(final Object value, final Options options) { - Integer width = options.param(0, 4); - notNull(width, "found 'null', expected 'indent'"); - return value.toString().trim().indent(width); + @NotNull + private static String createFileName(String name) { + return Label.of(name) + .lowerHyphen() + .replace(":", "-") + .concat(".adoc"); } - private boolean delimit(boolean first) { - if (!first) { - showCommand.main.printer().println("========================================"); + protected void printDescriptionUsing(ComponentService componentService, Class modelClass, String template, Writer writer) { + Class componentClass = componentService.getClass(); + Optional providesComponent = Annotations.find(componentClass, ProvidesComponent.class); + + if (providesComponent.isEmpty()) { + throw new IllegalStateException("component does not have a ProvidesComponent annotation: " + componentClass.getName()); } - return false; + Map params = Map.of( + "name", providesComponent.get().type(), + "synopsis", providesComponent.get().synopsis(), + "description", providesComponent.get().description(), + "model", getModel(modelClass) + ); + + showCommand.main.printer().writeWithTemplate(template, params, writer); } protected static String getModel(Class modelClass) { diff --git a/clusterless-main/src/main/resources/templates/components-adoc.hbs b/clusterless-main/src/main/resources/templates/components-adoc.hbs new file mode 100644 index 00000000..4eab6117 --- /dev/null +++ b/clusterless-main/src/main/resources/templates/components-adoc.hbs @@ -0,0 +1,20 @@ += Component + +Type: `{{name}}` + +== Synopsis + +{{{synopsis}}} + +{{#description~}} + == Description + .... + {{{description}}} + .... +{{/description~}} + +== Template: +[source,json] +---- +{{{model}}} +---- diff --git a/clusterless-main/src/main/resources/templates/components-cli.hbs b/clusterless-main/src/main/resources/templates/components-cli.hbs new file mode 100644 index 00000000..74661f4a --- /dev/null +++ b/clusterless-main/src/main/resources/templates/components-cli.hbs @@ -0,0 +1,11 @@ +Type: +{{name}} + +Synopsis: +{{{indent synopsis 4}}} +{{#description~}} + Description: + {{{indent description 4}}} +{{/description~}} +Template: +{{{model}}} diff --git a/clusterless-main/src/main/resources/templates/components-list-adoc.hbs b/clusterless-main/src/main/resources/templates/components-list-adoc.hbs new file mode 100644 index 00000000..349b88a2 --- /dev/null +++ b/clusterless-main/src/main/resources/templates/components-list-adoc.hbs @@ -0,0 +1,4 @@ +.{{title}} +{{#components~}} + * xref:{{filename}}[{{name}}] +{{/components~}} diff --git a/clusterless-main/src/main/resources/templates/components-list-partial-adoc.hbs b/clusterless-main/src/main/resources/templates/components-list-partial-adoc.hbs new file mode 100644 index 00000000..6a84bf7f --- /dev/null +++ b/clusterless-main/src/main/resources/templates/components-list-partial-adoc.hbs @@ -0,0 +1,3 @@ +{{#components~}} + * xref:reference:components:{{filename}}[{{name}}] +{{/components~}}