diff --git a/README.md b/README.md index 2e73f92..3262cf7 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,10 @@ Endgen currently contains two separate parsers: * endpoint - A DSL for expressing HTTP endpoints. * state - A DSL for expressing state and transitions. -Which parser that is used to read the input file is determined by the file name ending -'.endpoints' or '.states' or by a command line argument. +Only one parser will be sued when reading a file. Determined by the file name ending; +'.endpoints', or '.states', or by a command line argument. -The endpoint DSL and the state DSL share the grammar for expressing configuration and data types +The endpoint-DSL and the state-DSL share the grammar for expressing configuration and data types ,see below for details. ## How to Run @@ -89,15 +89,20 @@ a very limited DSL, you can for example not express what type of HTTP Verb to us no plans to extend the DSL to do that either. ## DSL -This is the ANTLR grammar for the root of the DSL +This is the ANTLR grammar for the root of the Endpoint-DSL ```antlrv4 document : generatorconfig? (namedTypeDeclaration|endpoint)* ; ``` +the corresponding grammar for the root of the State-DSL + +```antlrv4 +document : generatorconfig? transition (',' transition)* ; +``` + ### Configuration block -Meaning that the DSL file has an optional `generatorconfig` block at the top. Then you can write either; a type -definition, or an endpoint declaration, as many times as you like. +Both types of DSL files has an optional `generatorconfig` block at the top. Here is an example: ``` @@ -105,25 +110,32 @@ Here is an example: package: se.rutdev.senash, mykey: myvalue } +``` +This consists of a config block with 2 items, the 'package' and the 'mykey' definition. These are available to be used +in the freemarker template as a Map of String-keys to String-values. + +### Endpoint DSL +After the optional configuration block you can write either; a type definition, or an endpoint declaration, and repeat +as many times as you like. + +Here is an example: +``` /some/endpoint <- SomeType(foo:String) Embedded(foo:Bar) /some/other/endpoint <- (bar:Seq[Embedded]) ``` -This consists of a config block with 2 items, the 'package' and the 'mykey' definition. These are available to be used -in the freemarker template as a Map of String-keys to String-values. - ### Endpoint definition `/some/endpoint <- SomeType(foo:String)` is an endpoint definition. It declares one endpoint that have a request body data type called `SomeType` that has a field called `foo` of the type `String`. ### Data types -The DSL uses Scala convention of writing data types after the field name separated by a colon. Of course the DSL parser -does not know anything about java or scala types, as far as it is concerned these are 2 strings and the first one is -just named field-name and the other string is named field-type. +Both DSL-grammars use the Scala convention of writing data types after the field name separated by a colon. Of course +the parsers do not know anything about java or scala types, as far as the parser is concerned these are 2 strings and +the first one is just named: field-name and the other string is named: field-type. `Embedded(foo:Bar)` is a `namedTypeDeclaration` which is parsed the same way as the request type above. But isn't tied to a specific endpoint. @@ -148,55 +160,55 @@ It is possible to have an optional response data type declared like so: The right pointing arrow `->` denotes a response type, it can be an anonymous data type in which case the parser till name it from the last path segment and add 'Response' to the end of the data type name. -### State grammar - +### State DSL This is an example of a state file: ``` -start -> middle: message, -middle -> middle: selfmessage, -middle -> end: endmessage +start -> message -> middle , +middle -> selfmessage -> middle, +middle -> endmessage -> end ``` +The file declares 3 transitions. The first line states: Transition from the 'start' state to the 'middle' state with +the message 'message'. -It contains 3 state definitions `start`, `middle` and `end`. A state definition will be parsed as a data type with -the name of the state as the type name. - -It also contains 3 message definitions `message`, `selfmessage` and `endmessage`. Message definitions will also be -parsed as data types. +From this we can see that the file contains 3 state definitions `start`, `middle` and `end`. +A state definition will be parsed as a data type with the name of the state as the type name. It also contains 3 +message definitions `message`, `selfmessage` and `endmessage`. Message definitions will also be parsed as data types. Since the parser will extract datatypes it is possible to define the fields of the data types. This is a slightly more complicated example: ``` -start(foo:Foo) -> middle: message(a: String), -middle(bar:Bar) -> middle: selfmessage, -middle -> end: endmessage +start -> message -> middle, +middle -> selfmessage -> middle(bar:bar), +middle -> message -> end ``` -Where for example the data type for `middle` will have the field declaration with the name `bar` and the type `Bar`. +The data type for `middle` will have a field declaration with the name `bar` and the type `Bar`. Fields for the same state data type, or message data type, will be merged. Here is a complex example: ``` -start(s:S) -> middle(foo:foo): message(foo:foo), -middle -> middle(bar:bar): selfmessage(bar:bar), -middle -> end: message(bar:baz) +start(s:S) -> message(foo:foo) -> middle(foo:foo) , +middle -> selfmessage(bar:bar) -> middle(bar:bar), +middle -> message(bar:baz) -> end ``` -Not that we can declare fields on both the `from` and `to` state declarations. The `middle` datat type will have field +Note that we can declare fields on both the `from` and `to` state declarations. The `middle` datat type will have field definitons for `foo` and `bar`. The data type for `message` will have fields for `foo` and `bar`. -One restriction is that states and messages may not have the same name, i.e. be parsed as the same data type. +One restriction is that a state and a messages may share have the same name, i.e. be parsed as the same data type. ## Generating If the parser is successful it will hold the following data in the AST ```java -public record DocumentNode( - Map config, - Set typeDefinitions, - List endpoints, - Set states) { +public record GeneratorNode( + Map config, + Set typeDefinitions, + List endpoints, + Set states, + Meta meta) { } ``` @@ -221,8 +233,8 @@ that writes the value for a config key called 'package'. `package ${config.package}` ### Data types -These are all the data types the parser have collected, either from explicit declarations, request payloads and response -bodies. +These are all the data types the parser have collected, either from explicit declarations, request payloads, response +bodies, states or messages. ```java public record TypeNode(String name, List fields) { } @@ -281,11 +293,34 @@ The set of states will hold items of this shape: public record StateNode(String name, String data, Set transitions) { } ``` +* `name` is the name of the state. +* `data` is the name of the data type for the state. +* `transistions` are the outgoing arrows from the named state. -and the transitions has this structure: +Transitions have this structure: ```injectedfreemarker public record TransitionNode(String message, String toState) { } ``` +* `name` is the message name. +* `toState` is the name of the target state. +### Meta + +The meta container holds information about the template file used to generate the output file. +```java +public record Meta( + List templateDirectories, + String templateFile +) { } +``` + +* `templateDirectories` holds the subdirectory below the given `templateDir` where the template was found. +This is useful to generate e.g. java `package`statements like this: + +```injectedfreemarker +<#list meta.templateDirectories>package <#items as dir>${dir}<#sep>.; +``` + +* `templateFile` is the filename of the template (including the .ftl-ending) used to generate the output. \ No newline at end of file diff --git a/endgen-dist/pom.xml b/endgen-dist/pom.xml index 729e41a..8a2fedb 100644 --- a/endgen-dist/pom.xml +++ b/endgen-dist/pom.xml @@ -5,7 +5,7 @@ nu.zoom.dsl endgen - 1.1 + 1.3-SNAPSHOT endgen-dist diff --git a/endgen-maven-plugin/README.md b/endgen-maven-plugin/README.md new file mode 100644 index 0000000..3df592d --- /dev/null +++ b/endgen-maven-plugin/README.md @@ -0,0 +1,35 @@ +# Configure + +Add the following to your `pom.xml` + +```xml + + + + nu.zoom.dsl + endgen-maven-plugin + 1.2-SNAPSHOT + + + + endgen + + + ${project.basedir}/src/main/endpoint-templates + ${project.basedir}/src/main/endgen/test01.endpoints + + + + + + +``` + +Replace the `` with the latest published version of the endgen plugin. + +* `templates` should point to the template directory to use. +* `dsl` should be the file to generate code from. +* `output` can be used to specify the directory where the generated files are written. Default is `${project.build.directory}/generated-sources/endgen`. +* `parser` can be used to force the use of either the `Endpoints` or the `States` parser. Default is to determined by looking at the file ending of the dsl-file. + +If you have several DSL-files that you wish to generate from you can repeat the `` block with other configurations. \ No newline at end of file diff --git a/endgen-maven-plugin/pom.xml b/endgen-maven-plugin/pom.xml new file mode 100644 index 0000000..9974c25 --- /dev/null +++ b/endgen-maven-plugin/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + + nu.zoom.dsl + endgen + 1.3-SNAPSHOT + + endgen-maven-plugin + maven-plugin + + + 3.15.1 + + + + org.apache.maven + maven-plugin-api + 3.9.9 + provided + + + + org.apache.maven.plugin-tools + maven-plugin-annotations + ${maven-plugin-tools.version} + provided + + + nu.zoom.dsl + parser + ${project.parent.version} + + + \ No newline at end of file diff --git a/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/EndgenMojo.java b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/EndgenMojo.java new file mode 100644 index 0000000..757eee5 --- /dev/null +++ b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/EndgenMojo.java @@ -0,0 +1,68 @@ +package nu.zoom.dsl.maven; + +import nu.zoom.dsl.run.Runner; +import nu.zoom.dsl.run.ValidationException; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; + +import java.io.File; +import java.util.Optional; + +@Mojo( + name = "endgen", + defaultPhase = LifecyclePhase.GENERATE_SOURCES +) +public class EndgenMojo extends AbstractMojo { + @Parameter( + name = "templates", + defaultValue = "${project.build.sourceDirectory}/main/endgen-templates" + ) + File templates; + + @Parameter( + name = "output", + defaultValue = "${project.build.directory}/generated-sources/endgen" + ) + File output; + + @Parameter(name = "dsl", required = true) + File dsl; + + @Parameter(name = "parser") + String parser; + + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + try { + Runner.run( + optional(dsl).map(File::toPath).orElseThrow(), + optional(templates).map(File::toPath), + optional(output).map(File::toPath), + getParserType(parser), + new MavenLogger(getLog()) + ); + } catch (Exception e) { + throw new MojoExecutionException(e.getMessage(), e); + } + } + + private Optional getParserType(final String type) throws ValidationException { + if (type == null) { + return Optional.empty(); + } + try { + return Optional.of(Runner.ParserType.valueOf(type)); + } catch (IllegalArgumentException e) { + throw new ValidationException(e); + } + } + + private Optional optional(T arg) { + return arg == null ? Optional.empty() : Optional.of(arg); + } +} diff --git a/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/MavenLogger.java b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/MavenLogger.java new file mode 100644 index 0000000..03b98be --- /dev/null +++ b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/MavenLogger.java @@ -0,0 +1,17 @@ +package nu.zoom.dsl.maven; + +import nu.zoom.dsl.run.Logger; +import org.apache.maven.plugin.logging.Log; + +public class MavenLogger implements Logger { + private final Log delegate; + + public MavenLogger(Log delegate) { + this.delegate = delegate; + } + + @Override + public void println(String message) { + this.delegate.debug(message); + } +} diff --git a/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/Run.java b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/Run.java new file mode 100644 index 0000000..c8a0779 --- /dev/null +++ b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/Run.java @@ -0,0 +1,52 @@ +package nu.zoom.dsl.maven; + +import java.io.File; + +public class Run { + private File templates; + private File output; + private File dsl; + private String parser; + + public File getTemplates() { + return templates; + } + + public void setTemplates(File templates) { + this.templates = templates; + } + + public File getOutput() { + return output; + } + + public void setOutput(File output) { + this.output = output; + } + + public File getDsl() { + return dsl; + } + + public void setDsl(File dsl) { + this.dsl = dsl; + } + + public String getParser() { + return parser; + } + + public void setParser(String parser) { + this.parser = parser; + } + + @Override + public String toString() { + return "Run{" + + "templates=" + templates + + ", output=" + output + + ", dsl=" + dsl + + ", parser='" + parser + '\'' + + '}'; + } +} diff --git a/endpoints-templates/endpoints.txt.ftl b/endpoints-templates/endpoints.txt.ftl index 9a779fb..4904c00 100644 --- a/endpoints-templates/endpoints.txt.ftl +++ b/endpoints-templates/endpoints.txt.ftl @@ -1,3 +1,5 @@ +Generated from template: ${meta.templateFile} + <#list endpoints as endpoint> <#list endpoint.paths.paths> <#items as segment>/${segment} diff --git a/endpoints-templates/Codecs.scala.ftl b/endpoints-templates/nu/zoom/dsl/Codecs.scala.ftl similarity index 89% rename from endpoints-templates/Codecs.scala.ftl rename to endpoints-templates/nu/zoom/dsl/Codecs.scala.ftl index f11070f..7ea382d 100644 --- a/endpoints-templates/Codecs.scala.ftl +++ b/endpoints-templates/nu/zoom/dsl/Codecs.scala.ftl @@ -11,7 +11,7 @@ // 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 ${config.package} +<#list meta.templateDirectories>package <#items as dir>${dir}<#sep>.; object Codecs: <#list typeDefinitions as type> diff --git a/endpoints-templates/Endpoints.scala.ftl b/endpoints-templates/nu/zoom/dsl/Endpoints.scala.ftl similarity index 92% rename from endpoints-templates/Endpoints.scala.ftl rename to endpoints-templates/nu/zoom/dsl/Endpoints.scala.ftl index a68864c..fbc3538 100644 --- a/endpoints-templates/Endpoints.scala.ftl +++ b/endpoints-templates/nu/zoom/dsl/Endpoints.scala.ftl @@ -11,7 +11,7 @@ // 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 ${config.package} +<#list meta.templateDirectories>package<#items as dir>${dir}<#sep>.; class Endpoints: <#list endpoints as endpoint> diff --git a/endpoints-templates/Protocol.scala.ftl b/endpoints-templates/nu/zoom/dsl/Protocol.scala.ftl similarity index 90% rename from endpoints-templates/Protocol.scala.ftl rename to endpoints-templates/nu/zoom/dsl/Protocol.scala.ftl index faa5d55..41e6547 100644 --- a/endpoints-templates/Protocol.scala.ftl +++ b/endpoints-templates/nu/zoom/dsl/Protocol.scala.ftl @@ -11,7 +11,7 @@ // 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 ${config.package} +<#list meta.templateDirectories>package<#items as dir>${dir}<#sep>.; object Protocol: <#list typeDefinitions?sort as type> diff --git a/parser/pom.xml b/parser/pom.xml index 7444c5f..67c6925 100644 --- a/parser/pom.xml +++ b/parser/pom.xml @@ -20,7 +20,7 @@ nu.zoom.dsl endgen - 1.1 + 1.3-SNAPSHOT parser diff --git a/parser/src/main/antlr4/nu/zoom/dsl/parser/States.g4 b/parser/src/main/antlr4/nu/zoom/dsl/parser/States.g4 index dbfe628..d2a2b4d 100644 --- a/parser/src/main/antlr4/nu/zoom/dsl/parser/States.g4 +++ b/parser/src/main/antlr4/nu/zoom/dsl/parser/States.g4 @@ -15,7 +15,7 @@ grammar States; import Common; document : generatorconfig? transition (',' transition)* ; -transition : from RIGHT_ARROW to COLON message ; +transition : from RIGHT_ARROW message RIGHT_ARROW to ; from : state ; to : state ; message : typeName typeDeclaration? ; diff --git a/parser/src/main/java/nu/zoom/dsl/ast/DocumentNode.java b/parser/src/main/java/nu/zoom/dsl/ast/DocumentNode.java index fcb8621..7edb239 100644 --- a/parser/src/main/java/nu/zoom/dsl/ast/DocumentNode.java +++ b/parser/src/main/java/nu/zoom/dsl/ast/DocumentNode.java @@ -13,6 +13,7 @@ // limitations under the License. package nu.zoom.dsl.ast; +import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Set; diff --git a/parser/src/main/java/nu/zoom/dsl/ast/GeneratorNode.java b/parser/src/main/java/nu/zoom/dsl/ast/GeneratorNode.java new file mode 100644 index 0000000..2cea7ca --- /dev/null +++ b/parser/src/main/java/nu/zoom/dsl/ast/GeneratorNode.java @@ -0,0 +1,13 @@ +package nu.zoom.dsl.ast; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +public record GeneratorNode( + Map config, + Set typeDefinitions, + List endpoints, + Set states, + Meta meta) { +} diff --git a/parser/src/main/java/nu/zoom/dsl/ast/Meta.java b/parser/src/main/java/nu/zoom/dsl/ast/Meta.java new file mode 100644 index 0000000..567f3ab --- /dev/null +++ b/parser/src/main/java/nu/zoom/dsl/ast/Meta.java @@ -0,0 +1,9 @@ +package nu.zoom.dsl.ast; + +import java.util.List; + +public record Meta( + List templateDirectories, + String templateFile +) { +} diff --git a/parser/src/main/java/nu/zoom/dsl/cli/EndpointsCLI.java b/parser/src/main/java/nu/zoom/dsl/cli/EndpointsCLI.java index e3d5794..8d79757 100644 --- a/parser/src/main/java/nu/zoom/dsl/cli/EndpointsCLI.java +++ b/parser/src/main/java/nu/zoom/dsl/cli/EndpointsCLI.java @@ -14,7 +14,7 @@ package nu.zoom.dsl.cli; import nu.zoom.dsl.ast.DocumentNode; -import nu.zoom.dsl.ast.ParserWrapper; +import nu.zoom.dsl.run.*; import nu.zoom.dsl.freemarker.Generator; import picocli.CommandLine; import picocli.CommandLine.Command; @@ -24,8 +24,8 @@ import picocli.CommandLine.Parameters; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.List; +import java.util.Optional; import java.util.concurrent.Callable; @Command( @@ -34,20 +34,16 @@ import java.util.concurrent.Callable; description = "Generate source code from an endpoints specification file." ) public class EndpointsCLI implements Callable { - public enum ParserType { - Endpoints, - States - } @SuppressWarnings("unused") @Parameters(index = "0", description = "The source endpoints DSL file.") private Path file; @SuppressWarnings("CanBeFinal") - @Option(names = {"-t", "--template"}, defaultValue = "endpoints-template", description = "The template directory. Default is ${DEFAULT-VALUE}") + @Option(names = {"-t", "--template"}, defaultValue = Runner.DEFAULT_TEMPLATE_DIRECTORY_NAME, description = "The template directory. Default is ${DEFAULT-VALUE}") private Path templateDir ; @SuppressWarnings("CanBeFinal") - @Option(names = {"-o", "--output"}, defaultValue = "endpoints-output", description = "The directory to write the generated code to. Default is ${DEFAULT-VALUE}") + @Option(names = {"-o", "--output"}, defaultValue = Runner.DEFAULT_OUTPUT_DIRECTORY_NAME, description = "The directory to write the generated code to. Default is ${DEFAULT-VALUE}") private Path outputDir ; @SuppressWarnings("unused") @@ -55,7 +51,7 @@ public class EndpointsCLI implements Callable { private Boolean verbose = false; @Option(names = {"-p", "--parser"}, description = "Force use of a specific parser instead of determining from filename. Valid values: ${COMPLETION-CANDIDATES}.") - private ParserType parser = null; + private Runner.ParserType parser = null; public static void main(String[] args) { int exitCode = new CommandLine(new EndpointsCLI()).execute(args); @@ -65,32 +61,14 @@ public class EndpointsCLI implements Callable { @Override public Integer call() { try { - validateTemplateDirectory(); - validateInputFile(); - validateOutputDirectory(); - verbose("Parsing: " + file.toAbsolutePath()); - if (parser == null) { - if (file.getFileName().toString().endsWith(".states")) { - parser = ParserType.States; - } - } - final DocumentNode rootNode ; - if (parser == ParserType.States) { - verbose("using state grammar.") ; - rootNode = ParserWrapper.parseStates(file); - } else { - verbose("using endpoints grammar.") ; - rootNode = ParserWrapper.parseEndpoints(file); - } - verbose("AST: " + rootNode); - verbose("Generating from templates in: " + templateDir.toAbsolutePath()); - Generator generator = new Generator(templateDir, rootNode, outputDir); - List generatedPaths = generator.generate(); - if (generatedPaths.isEmpty()) { - System.out.println("No generated paths found."); - } else { - generatedPaths.forEach(p -> verbose("Generated: " + p.toAbsolutePath())); - } + final Logger logger = this.verbose ? new StdoutLogger() : new NullLogger() ; + Runner.run( + this.file, + Optional.of(this.templateDir), + Optional.of(this.outputDir), + parser == null ? Optional.empty() : Optional.of(parser), + logger + ); return 0; } catch (Exception e) { System.err.println(e.getMessage()); diff --git a/parser/src/main/java/nu/zoom/dsl/freemarker/Generator.java b/parser/src/main/java/nu/zoom/dsl/freemarker/Generator.java index dcfa86a..0458669 100644 --- a/parser/src/main/java/nu/zoom/dsl/freemarker/Generator.java +++ b/parser/src/main/java/nu/zoom/dsl/freemarker/Generator.java @@ -18,6 +18,8 @@ import freemarker.template.Template; import freemarker.template.TemplateException; import freemarker.template.TemplateExceptionHandler; import nu.zoom.dsl.ast.DocumentNode; +import nu.zoom.dsl.ast.GeneratorNode; +import nu.zoom.dsl.ast.Meta; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -26,51 +28,72 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; import java.util.stream.Stream; public class Generator { - private final Path templatesDir; - private final DocumentNode data; - private final Path outputDir; - private final Configuration cfg; - private final String TEMPLATE_EXTENSION = ".ftl"; - private final int TEMPLATE_EXTENSION_LENGTH = TEMPLATE_EXTENSION.length(); + private final Path templatesDir; + private final DocumentNode documentNode; + private final Path outputDir; + private final Configuration cfg; + private final String TEMPLATE_EXTENSION = ".ftl"; + private final int TEMPLATE_EXTENSION_LENGTH = TEMPLATE_EXTENSION.length(); - public Generator(Path templatesDir, DocumentNode data, Path outputDir) throws IOException { - this.templatesDir = Objects.requireNonNull(templatesDir); - this.data = Objects.requireNonNull(data); - this.outputDir = Objects.requireNonNull(outputDir); - this.cfg = new Configuration(Configuration.VERSION_2_3_34); - cfg.setDirectoryForTemplateLoading(templatesDir.toFile()); - cfg.setDefaultEncoding("UTF-8"); - cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); - cfg.setLogTemplateExceptions(false); - cfg.setWrapUncheckedExceptions(true); - cfg.setFallbackOnNullLoopVariable(false); - } + public Generator(Path templatesDir, DocumentNode documentNode, Path outputDir) throws IOException { + this.templatesDir = Objects.requireNonNull(templatesDir); + this.documentNode = Objects.requireNonNull(documentNode); + this.outputDir = Objects.requireNonNull(outputDir); + this.cfg = new Configuration(Configuration.VERSION_2_3_34); + cfg.setDirectoryForTemplateLoading(templatesDir.toFile()); + cfg.setDefaultEncoding("UTF-8"); + cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + cfg.setLogTemplateExceptions(false); + cfg.setWrapUncheckedExceptions(true); + cfg.setFallbackOnNullLoopVariable(false); + } - public List generate() throws IOException, TemplateException { - try (Stream files = Files.list(templatesDir)) { - List templates = files - .map(Path::getFileName) - .map(Path::toString) - .filter(p -> p.length() > TEMPLATE_EXTENSION_LENGTH && p.endsWith(TEMPLATE_EXTENSION) - ) - .toList(); - ArrayList out = new ArrayList<>(); - for (String template : templates) { - Path outpath = outputDir.resolve(outputFilenameFromTemplate(template)); - Template ftl = this.cfg.getTemplate(template); - try (var outw = Files.newBufferedWriter(outpath, StandardCharsets.UTF_8)) { - ftl.process(this.data, outw); - out.add(outpath); - } - } - return out; - } - } + public List generate() throws IOException, TemplateException { + try (Stream files = Files.walk(templatesDir)) { + List templates = files + .filter(p -> { + var fname = p.getFileName().toString(); + return fname.length() > TEMPLATE_EXTENSION_LENGTH && fname.endsWith(TEMPLATE_EXTENSION); + } + ) + .map(p -> templatesDir.relativize(p)) + .toList(); + ArrayList out = new ArrayList<>(); + for (Path template : templates) { + Path outpath = outputDir.resolve(outputFilenameFromTemplate(template.toString())); + Files.createDirectories(outpath.getParent()); + Path templateSubdirectory = template.getParent(); + ArrayList templateDirectories= new ArrayList<>() ; + if (templateSubdirectory != null) { + var ti = templateSubdirectory.iterator(); + while (ti.hasNext()) { + templateDirectories.add(ti.next().toString()); + } + } + String templateName = template.getFileName().toString(); + Meta meta = new Meta(templateDirectories, templateName); + GeneratorNode generatorNode = new GeneratorNode( + this.documentNode.config(), + this.documentNode.typeDefinitions(), + this.documentNode.endpoints(), + this.documentNode.states(), + meta + ); + Template ftl = this.cfg.getTemplate(template.toString()); + try (var outw = Files.newBufferedWriter(outpath, StandardCharsets.UTF_8)) { + ftl.process(generatorNode, outw); + out.add(outpath); + } + } + return out; + } + } - private String outputFilenameFromTemplate(String template) { - return template.substring(0, template.length() - TEMPLATE_EXTENSION_LENGTH); - } + private String outputFilenameFromTemplate(String template) { + return template.substring(0, template.length() - TEMPLATE_EXTENSION_LENGTH); + } } diff --git a/parser/src/main/java/nu/zoom/dsl/run/EndgenException.java b/parser/src/main/java/nu/zoom/dsl/run/EndgenException.java new file mode 100644 index 0000000..7771e11 --- /dev/null +++ b/parser/src/main/java/nu/zoom/dsl/run/EndgenException.java @@ -0,0 +1,15 @@ +package nu.zoom.dsl.run; + +public abstract class EndgenException extends Exception { + public EndgenException(String message, Throwable cause) { + super(message, cause); + } + + public EndgenException(Throwable cause) { + super(cause); + } + + public EndgenException(String message) { + super(message); + } +} diff --git a/parser/src/main/java/nu/zoom/dsl/run/GeneratorException.java b/parser/src/main/java/nu/zoom/dsl/run/GeneratorException.java new file mode 100644 index 0000000..5157d97 --- /dev/null +++ b/parser/src/main/java/nu/zoom/dsl/run/GeneratorException.java @@ -0,0 +1,15 @@ +package nu.zoom.dsl.run; + +public class GeneratorException extends EndgenException { + public GeneratorException(String message) { + super(message); + } + + public GeneratorException(String message, Throwable cause) { + super(message, cause); + } + + public GeneratorException(Throwable cause) { + super(cause); + } +} diff --git a/parser/src/main/java/nu/zoom/dsl/run/Logger.java b/parser/src/main/java/nu/zoom/dsl/run/Logger.java new file mode 100644 index 0000000..80826d3 --- /dev/null +++ b/parser/src/main/java/nu/zoom/dsl/run/Logger.java @@ -0,0 +1,5 @@ +package nu.zoom.dsl.run; + +public interface Logger { + void println(String message); +} diff --git a/parser/src/main/java/nu/zoom/dsl/run/NullLogger.java b/parser/src/main/java/nu/zoom/dsl/run/NullLogger.java new file mode 100644 index 0000000..014f473 --- /dev/null +++ b/parser/src/main/java/nu/zoom/dsl/run/NullLogger.java @@ -0,0 +1,8 @@ +package nu.zoom.dsl.run; + +public class NullLogger implements Logger { + @Override + public void println(String message) { + + } +} diff --git a/parser/src/main/java/nu/zoom/dsl/run/ParserException.java b/parser/src/main/java/nu/zoom/dsl/run/ParserException.java new file mode 100644 index 0000000..b726384 --- /dev/null +++ b/parser/src/main/java/nu/zoom/dsl/run/ParserException.java @@ -0,0 +1,15 @@ +package nu.zoom.dsl.run; + +public class ParserException extends EndgenException { + public ParserException(String message) { + super(message); + } + + public ParserException(String message, Throwable cause) { + super(message, cause); + } + + public ParserException(Throwable cause) { + super(cause); + } +} diff --git a/parser/src/main/java/nu/zoom/dsl/ast/ParserWrapper.java b/parser/src/main/java/nu/zoom/dsl/run/ParserWrapper.java similarity index 93% rename from parser/src/main/java/nu/zoom/dsl/ast/ParserWrapper.java rename to parser/src/main/java/nu/zoom/dsl/run/ParserWrapper.java index 11a95fd..ce62d9c 100644 --- a/parser/src/main/java/nu/zoom/dsl/ast/ParserWrapper.java +++ b/parser/src/main/java/nu/zoom/dsl/run/ParserWrapper.java @@ -11,8 +11,11 @@ // 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 nu.zoom.dsl.ast; +package nu.zoom.dsl.run; +import nu.zoom.dsl.ast.DocumentNode; +import nu.zoom.dsl.ast.EndpointsVisitorTransformer; +import nu.zoom.dsl.ast.StatesVisitorTransformer; import nu.zoom.dsl.parser.*; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; diff --git a/parser/src/main/java/nu/zoom/dsl/run/Runner.java b/parser/src/main/java/nu/zoom/dsl/run/Runner.java new file mode 100644 index 0000000..ebf162b --- /dev/null +++ b/parser/src/main/java/nu/zoom/dsl/run/Runner.java @@ -0,0 +1,96 @@ +package nu.zoom.dsl.run; + +import freemarker.template.TemplateException; +import nu.zoom.dsl.ast.DocumentNode; +import nu.zoom.dsl.freemarker.Generator; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; + +public final class Runner { + public static final String DEFAULT_TEMPLATE_DIRECTORY_NAME = "endpoints-template"; + public static final String DEFAULT_OUTPUT_DIRECTORY_NAME = "endpoints-output"; + + public static void run( + Path dsl, + Optional templates, + Optional output, + Optional maybeParser, + Logger logger + ) throws ValidationException, IOException, GeneratorException { + Path templatesDir = templates.orElse(Paths.get(DEFAULT_TEMPLATE_DIRECTORY_NAME)); + Path outputDir = output.orElse(Paths.get(DEFAULT_OUTPUT_DIRECTORY_NAME)); + + validateOutputDirectory(outputDir); + validateTemplateDirectory(templatesDir); + validateInputFile(dsl); + logger.println("Parsing: " + dsl.toAbsolutePath()); + + final ParserType parser = + maybeParser.orElseGet( + () -> { + if (dsl.getFileName().toString().endsWith(".states")) { + return ParserType.States; + } else { + return ParserType.Endpoints; + } + } + ); + + final DocumentNode rootNode; + if (parser == ParserType.States) { + logger.println("using state grammar."); + rootNode = ParserWrapper.parseStates(dsl); + } else { + logger.println("using endpoints grammar."); + rootNode = ParserWrapper.parseEndpoints(dsl); + } + logger.println("AST: " + rootNode); + logger.println("Generating from templates in: " + templatesDir.toAbsolutePath()); + Generator generator = new Generator(templatesDir, rootNode, outputDir); + List generatedPaths = null; + try { + generatedPaths = generator.generate(); + } catch (TemplateException e) { + throw new GeneratorException(e); + } + if (generatedPaths.isEmpty()) { + System.out.println("No generated paths found."); + } else { + generatedPaths.forEach(p -> logger.println("Generated: " + p.toAbsolutePath())); + } + } + + private static void validateOutputDirectory(Path outputDir) throws IOException, ValidationException { + if (Files.notExists(outputDir)) { + Files.createDirectories(outputDir); + } + if (!Files.isDirectory(outputDir)) { + throw new ValidationException("Output directory: '" + outputDir + " 'is not a directory."); + } + } + + private static void validateTemplateDirectory(Path templateDir) throws ValidationException { + if (!Files.isDirectory(templateDir)) { + throw new ValidationException("Template directory '" + templateDir + "' is not a directory."); + } + } + + private static void validateInputFile(Path file) throws ValidationException { + if (Files.notExists(file)) { + throw new ValidationException("Input file '" + file + "' does not exist."); + } + if (!Files.isReadable(file) || !Files.isRegularFile(file)) { + throw new ValidationException("Input file '" + file + "' is not a readable file."); + } + } + + public enum ParserType { + Endpoints, + States + } +} diff --git a/parser/src/main/java/nu/zoom/dsl/run/StdoutLogger.java b/parser/src/main/java/nu/zoom/dsl/run/StdoutLogger.java new file mode 100644 index 0000000..7b0b7d4 --- /dev/null +++ b/parser/src/main/java/nu/zoom/dsl/run/StdoutLogger.java @@ -0,0 +1,9 @@ +package nu.zoom.dsl.run; + +public class StdoutLogger implements Logger { + + @Override + public void println(String message) { + System.out.println(message); + } +} diff --git a/parser/src/main/java/nu/zoom/dsl/run/ValidationException.java b/parser/src/main/java/nu/zoom/dsl/run/ValidationException.java new file mode 100644 index 0000000..1627d72 --- /dev/null +++ b/parser/src/main/java/nu/zoom/dsl/run/ValidationException.java @@ -0,0 +1,15 @@ +package nu.zoom.dsl.run; + +public class ValidationException extends EndgenException { + public ValidationException(String message) { + super(message); + } + + public ValidationException(String message, Throwable cause) { + super(message, cause); + } + + public ValidationException(Throwable cause) { + super(cause); + } +} diff --git a/pom.xml b/pom.xml index f53798f..82a7a48 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ nu.zoom.dsl endgen - 1.1 + 1.3-SNAPSHOT pom @@ -59,12 +59,13 @@ scm:git:https://codeberg.org/darkstar/endgen.git scm:git:ssh://git@vcs.zoom.nu:1122/zoom/endgen.git - v1.1 + admin-parent-1.1 parser endgen-dist + endgen-maven-plugin diff --git a/sample-maven/pom.xml b/sample-maven/pom.xml new file mode 100644 index 0000000..99fdbab --- /dev/null +++ b/sample-maven/pom.xml @@ -0,0 +1,70 @@ + + + + 4.0.0 + + nu.zoom.dsl + sample-maven + 1.0-SNAPSHOT + jar + + + 21 + 21 + UTF-8 + + + + + ASF 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + + + + + Johan Maasing + johan@zoom.nu + + developer + + + + + + + + nu.zoom.dsl + endgen-maven-plugin + 1.2-SNAPSHOT + + + + endgen + + + ${project.basedir}/src/main/endpoint-templates + ${project.basedir}/src/main/endgen/test01.endpoints + + + + + + + + diff --git a/sample-maven/src/main/endgen/test01.endpoints b/sample-maven/src/main/endgen/test01.endpoints new file mode 100644 index 0000000..6f212ed --- /dev/null +++ b/sample-maven/src/main/endgen/test01.endpoints @@ -0,0 +1,28 @@ +/* + Copyright 2025 "Johan Maasing" + + 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 + + http://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. +*/ + +{ + some: configvalue, + someother: value, + package: se.rutdev.senash +} + +/some/endpoint <- SomeType(foo:String) +Embedded(foo:Bar) +/some/other/endpoint <- (bar:Seq[Embedded]) +/yet/other/endpoint2 <- (bar2:Seq[AType]) -> NamedResponse(foo:Bar) +AType(data: java.util.List) +/yet/other/endpoint3 <- (bar2:Seq[AType]) -> (foo:Bar) diff --git a/sample-maven/src/main/endgen/test01.states b/sample-maven/src/main/endgen/test01.states new file mode 100644 index 0000000..3b9a67a --- /dev/null +++ b/sample-maven/src/main/endgen/test01.states @@ -0,0 +1,21 @@ +/* + Copyright 2025 "Johan Maasing" + + 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 + + http://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. +*/ + +{ title: SomeNodes, package: nu.zoom.dsl.states } + +start(s:S) -> message(foo:foo) -> middle(foo:foo) , +middle -> selfmessage(bar:bar) -> middle(bar:bar), +middle -> message(bar:baz) -> end diff --git a/sample-maven/src/main/endpoint-templates/nu/zoom/dsl/sample/Endpoints.java.ftl b/sample-maven/src/main/endpoint-templates/nu/zoom/dsl/sample/Endpoints.java.ftl new file mode 100644 index 0000000..624c722 --- /dev/null +++ b/sample-maven/src/main/endpoint-templates/nu/zoom/dsl/sample/Endpoints.java.ftl @@ -0,0 +1,22 @@ +// Copyright 2025 "Johan Maasing" +// +// 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 +// +// http://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. +<#list meta.templateDirectories>package<#items as dir>${dir}<#sep>.; + +class Endpoints() { +<#list endpoints as endpoint> + /* <#list endpoint.paths.paths><#items as segment>/${segment} */ + public void handle${endpoint.inputType?cap_first} + + +} \ No newline at end of file diff --git a/test01.states b/test01.states index bcb304f..3b9a67a 100644 --- a/test01.states +++ b/test01.states @@ -16,6 +16,6 @@ { title: SomeNodes, package: nu.zoom.dsl.states } -start(s:S) -> middle(foo:foo): message(foo:foo), -middle -> middle(bar:bar): selfmessage(bar:bar), -middle -> end: message(bar:baz) +start(s:S) -> message(foo:foo) -> middle(foo:foo) , +middle -> selfmessage(bar:bar) -> middle(bar:bar), +middle -> message(bar:baz) -> end