Compare commits

..

11 commits
v1.1 ... main

33 changed files with 719 additions and 127 deletions

113
README.md
View file

@ -37,10 +37,10 @@ Endgen currently contains two separate parsers:
* endpoint - A DSL for expressing HTTP endpoints. * endpoint - A DSL for expressing HTTP endpoints.
* state - A DSL for expressing state and transitions. * 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 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. '.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. ,see below for details.
## How to Run ## 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. no plans to extend the DSL to do that either.
## DSL ## 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 ```antlrv4
document : generatorconfig? (namedTypeDeclaration|endpoint)* ; document : generatorconfig? (namedTypeDeclaration|endpoint)* ;
``` ```
the corresponding grammar for the root of the State-DSL
```antlrv4
document : generatorconfig? transition (',' transition)* ;
```
### Configuration block ### Configuration block
Meaning that the DSL file has an optional `generatorconfig` block at the top. Then you can write either; a type Both types of DSL files has an optional `generatorconfig` block at the top.
definition, or an endpoint declaration, as many times as you like.
Here is an example: Here is an example:
``` ```
@ -105,25 +110,32 @@ Here is an example:
package: se.rutdev.senash, package: se.rutdev.senash,
mykey: myvalue 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) /some/endpoint <- SomeType(foo:String)
Embedded(foo:Bar) Embedded(foo:Bar)
/some/other/endpoint <- (bar:Seq[Embedded]) /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 ### Endpoint definition
`/some/endpoint <- SomeType(foo:String)` is an endpoint definition. It declares one endpoint that have a request body `/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 type called `SomeType` that has a field called `foo` of the type `String`.
### Data types ### Data types
The DSL uses Scala convention of writing data types after the field name separated by a colon. Of course the DSL parser Both DSL-grammars use the Scala convention of writing data types after the field name separated by a colon. Of course
does not know anything about java or scala types, as far as it is concerned these are 2 strings and the first one is the parsers do not know anything about java or scala types, as far as the parser is concerned these are 2 strings and
just named field-name and the other string is named field-type. 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 `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. 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 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. 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: This is an example of a state file:
``` ```
start -> middle: message, start -> message -> middle ,
middle -> middle: selfmessage, middle -> selfmessage -> middle,
middle -> end: endmessage 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 From this we can see that the file contains 3 state definitions `start`, `middle` and `end`.
the name of the state as the type name. 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.
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 Since the parser will extract datatypes it is possible to define the fields of the data types. This is a slightly more
complicated example: complicated example:
``` ```
start(foo:Foo) -> middle: message(a: String), start -> message -> middle,
middle(bar:Bar) -> middle: selfmessage, middle -> selfmessage -> middle(bar:bar),
middle -> end: endmessage 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: 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), start(s:S) -> message(foo:foo) -> middle(foo:foo) ,
middle -> middle(bar:bar): selfmessage(bar:bar), middle -> selfmessage(bar:bar) -> middle(bar:bar),
middle -> end: message(bar:baz) 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`. definitons for `foo` and `bar`.
The data type for `message` will have fields 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 ## Generating
If the parser is successful it will hold the following data in the AST If the parser is successful it will hold the following data in the AST
```java ```java
public record DocumentNode( public record GeneratorNode(
Map<String, String> config, Map<String, String> config,
Set<TypeNode> typeDefinitions, Set<TypeNode> typeDefinitions,
List<EndpointNode> endpoints, List<EndpointNode> endpoints,
Set<StateNode> states) { Set<StateNode> states,
Meta meta) {
} }
``` ```
@ -221,8 +233,8 @@ that writes the value for a config key called 'package'.
`package ${config.package}` `package ${config.package}`
### Data types ### Data types
These are all the data types the parser have collected, either from explicit declarations, request payloads and response These are all the data types the parser have collected, either from explicit declarations, request payloads, response
bodies. bodies, states or messages.
```java ```java
public record TypeNode(String name, List<FieldNode> fields) { } public record TypeNode(String name, List<FieldNode> fields) { }
@ -281,11 +293,34 @@ The set of states will hold items of this shape:
public record StateNode(String name, String data, Set<TransitionNode> transitions) { public record StateNode(String name, String data, Set<TransitionNode> 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 ```injectedfreemarker
public record TransitionNode(String message, String toState) { 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<String> 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>.</#items>;</#list>
```
* `templateFile` is the filename of the template (including the .ftl-ending) used to generate the output.

View file

@ -5,7 +5,7 @@
<parent> <parent>
<groupId>nu.zoom.dsl</groupId> <groupId>nu.zoom.dsl</groupId>
<artifactId>endgen</artifactId> <artifactId>endgen</artifactId>
<version>1.1</version> <version>1.3-SNAPSHOT</version>
</parent> </parent>
<artifactId>endgen-dist</artifactId> <artifactId>endgen-dist</artifactId>

View file

@ -0,0 +1,35 @@
# Configure
Add the following to your `pom.xml`
```xml
<build>
<plugins>
<plugin>
<groupId>nu.zoom.dsl</groupId>
<artifactId>endgen-maven-plugin</artifactId>
<version>1.2-SNAPSHOT</version>
<executions>
<execution>
<goals>
<goal>endgen</goal>
</goals>
<configuration>
<templates>${project.basedir}/src/main/endpoint-templates</templates>
<dsl>${project.basedir}/src/main/endgen/test01.endpoints</dsl>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
```
Replace the `<version>` 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 `<execution>` block with other configurations.

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>nu.zoom.dsl</groupId>
<artifactId>endgen</artifactId>
<version>1.3-SNAPSHOT</version>
</parent>
<artifactId>endgen-maven-plugin</artifactId>
<packaging>maven-plugin</packaging>
<properties>
<maven-plugin-tools.version>3.15.1</maven-plugin-tools.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>3.9.9</version>
<scope>provided</scope>
</dependency>
<!-- dependency on annotations -->
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>${maven-plugin-tools.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>nu.zoom.dsl</groupId>
<artifactId>parser</artifactId>
<version>${project.parent.version}</version>
</dependency>
</dependencies>
</project>

View file

@ -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<Runner.ParserType> 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 <T> Optional<T> optional(T arg) {
return arg == null ? Optional.empty() : Optional.of(arg);
}
}

View file

@ -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);
}
}

View file

@ -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 + '\'' +
'}';
}
}

View file

@ -1,3 +1,5 @@
Generated from template: ${meta.templateFile}
<#list endpoints as endpoint> <#list endpoints as endpoint>
<#list endpoint.paths.paths> <#list endpoint.paths.paths>
<#items as segment>/${segment}</#items> <#items as segment>/${segment}</#items>

View file

@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ${config.package} <#list meta.templateDirectories>package <#items as dir>${dir}<#sep>.</#items>;</#list>
object Codecs: object Codecs:
<#list typeDefinitions as type> <#list typeDefinitions as type>

View file

@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ${config.package} <#list meta.templateDirectories>package<#items as dir>${dir}<#sep>.</#items>;</#list>
class Endpoints: class Endpoints:
<#list endpoints as endpoint> <#list endpoints as endpoint>

View file

@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package ${config.package} <#list meta.templateDirectories>package<#items as dir>${dir}<#sep>.</#items>;</#list>
object Protocol: object Protocol:
<#list typeDefinitions?sort as type> <#list typeDefinitions?sort as type>

View file

@ -20,7 +20,7 @@
<parent> <parent>
<groupId>nu.zoom.dsl</groupId> <groupId>nu.zoom.dsl</groupId>
<artifactId>endgen</artifactId> <artifactId>endgen</artifactId>
<version>1.1</version> <version>1.3-SNAPSHOT</version>
</parent> </parent>
<artifactId>parser</artifactId> <artifactId>parser</artifactId>

View file

@ -15,7 +15,7 @@ grammar States;
import Common; import Common;
document : generatorconfig? transition (',' transition)* ; document : generatorconfig? transition (',' transition)* ;
transition : from RIGHT_ARROW to COLON message ; transition : from RIGHT_ARROW message RIGHT_ARROW to ;
from : state ; from : state ;
to : state ; to : state ;
message : typeName typeDeclaration? ; message : typeName typeDeclaration? ;

View file

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
package nu.zoom.dsl.ast; package nu.zoom.dsl.ast;
import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;

View file

@ -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<String, String> config,
Set<TypeNode> typeDefinitions,
List<EndpointNode> endpoints,
Set<StateNode> states,
Meta meta) {
}

View file

@ -0,0 +1,9 @@
package nu.zoom.dsl.ast;
import java.util.List;
public record Meta(
List<String> templateDirectories,
String templateFile
) {
}

View file

@ -14,7 +14,7 @@
package nu.zoom.dsl.cli; package nu.zoom.dsl.cli;
import nu.zoom.dsl.ast.DocumentNode; import nu.zoom.dsl.ast.DocumentNode;
import nu.zoom.dsl.ast.ParserWrapper; import nu.zoom.dsl.run.*;
import nu.zoom.dsl.freemarker.Generator; import nu.zoom.dsl.freemarker.Generator;
import picocli.CommandLine; import picocli.CommandLine;
import picocli.CommandLine.Command; import picocli.CommandLine.Command;
@ -24,8 +24,8 @@ import picocli.CommandLine.Parameters;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
@Command( @Command(
@ -34,20 +34,16 @@ import java.util.concurrent.Callable;
description = "Generate source code from an endpoints specification file." description = "Generate source code from an endpoints specification file."
) )
public class EndpointsCLI implements Callable<Integer> { public class EndpointsCLI implements Callable<Integer> {
public enum ParserType {
Endpoints,
States
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
@Parameters(index = "0", description = "The source endpoints DSL file.") @Parameters(index = "0", description = "The source endpoints DSL file.")
private Path file; private Path file;
@SuppressWarnings("CanBeFinal") @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 ; private Path templateDir ;
@SuppressWarnings("CanBeFinal") @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 ; private Path outputDir ;
@SuppressWarnings("unused") @SuppressWarnings("unused")
@ -55,7 +51,7 @@ public class EndpointsCLI implements Callable<Integer> {
private Boolean verbose = false; private Boolean verbose = false;
@Option(names = {"-p", "--parser"}, description = "Force use of a specific parser instead of determining from filename. Valid values: ${COMPLETION-CANDIDATES}.") @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) { public static void main(String[] args) {
int exitCode = new CommandLine(new EndpointsCLI()).execute(args); int exitCode = new CommandLine(new EndpointsCLI()).execute(args);
@ -65,32 +61,14 @@ public class EndpointsCLI implements Callable<Integer> {
@Override @Override
public Integer call() { public Integer call() {
try { try {
validateTemplateDirectory(); final Logger logger = this.verbose ? new StdoutLogger() : new NullLogger() ;
validateInputFile(); Runner.run(
validateOutputDirectory(); this.file,
verbose("Parsing: " + file.toAbsolutePath()); Optional.of(this.templateDir),
if (parser == null) { Optional.of(this.outputDir),
if (file.getFileName().toString().endsWith(".states")) { parser == null ? Optional.empty() : Optional.of(parser),
parser = ParserType.States; logger
} );
}
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<Path> generatedPaths = generator.generate();
if (generatedPaths.isEmpty()) {
System.out.println("No generated paths found.");
} else {
generatedPaths.forEach(p -> verbose("Generated: " + p.toAbsolutePath()));
}
return 0; return 0;
} catch (Exception e) { } catch (Exception e) {
System.err.println(e.getMessage()); System.err.println(e.getMessage());

View file

@ -18,6 +18,8 @@ import freemarker.template.Template;
import freemarker.template.TemplateException; import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler; import freemarker.template.TemplateExceptionHandler;
import nu.zoom.dsl.ast.DocumentNode; import nu.zoom.dsl.ast.DocumentNode;
import nu.zoom.dsl.ast.GeneratorNode;
import nu.zoom.dsl.ast.Meta;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -26,51 +28,72 @@ import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
public class Generator { public class Generator {
private final Path templatesDir; private final Path templatesDir;
private final DocumentNode data; private final DocumentNode documentNode;
private final Path outputDir; private final Path outputDir;
private final Configuration cfg; private final Configuration cfg;
private final String TEMPLATE_EXTENSION = ".ftl"; private final String TEMPLATE_EXTENSION = ".ftl";
private final int TEMPLATE_EXTENSION_LENGTH = TEMPLATE_EXTENSION.length(); private final int TEMPLATE_EXTENSION_LENGTH = TEMPLATE_EXTENSION.length();
public Generator(Path templatesDir, DocumentNode data, Path outputDir) throws IOException { public Generator(Path templatesDir, DocumentNode documentNode, Path outputDir) throws IOException {
this.templatesDir = Objects.requireNonNull(templatesDir); this.templatesDir = Objects.requireNonNull(templatesDir);
this.data = Objects.requireNonNull(data); this.documentNode = Objects.requireNonNull(documentNode);
this.outputDir = Objects.requireNonNull(outputDir); this.outputDir = Objects.requireNonNull(outputDir);
this.cfg = new Configuration(Configuration.VERSION_2_3_34); this.cfg = new Configuration(Configuration.VERSION_2_3_34);
cfg.setDirectoryForTemplateLoading(templatesDir.toFile()); cfg.setDirectoryForTemplateLoading(templatesDir.toFile());
cfg.setDefaultEncoding("UTF-8"); cfg.setDefaultEncoding("UTF-8");
cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
cfg.setLogTemplateExceptions(false); cfg.setLogTemplateExceptions(false);
cfg.setWrapUncheckedExceptions(true); cfg.setWrapUncheckedExceptions(true);
cfg.setFallbackOnNullLoopVariable(false); cfg.setFallbackOnNullLoopVariable(false);
} }
public List<Path> generate() throws IOException, TemplateException { public List<Path> generate() throws IOException, TemplateException {
try (Stream<Path> files = Files.list(templatesDir)) { try (Stream<Path> files = Files.walk(templatesDir)) {
List<String> templates = files List<Path> templates = files
.map(Path::getFileName) .filter(p -> {
.map(Path::toString) var fname = p.getFileName().toString();
.filter(p -> p.length() > TEMPLATE_EXTENSION_LENGTH && p.endsWith(TEMPLATE_EXTENSION) return fname.length() > TEMPLATE_EXTENSION_LENGTH && fname.endsWith(TEMPLATE_EXTENSION);
) }
.toList(); )
ArrayList<Path> out = new ArrayList<>(); .map(p -> templatesDir.relativize(p))
for (String template : templates) { .toList();
Path outpath = outputDir.resolve(outputFilenameFromTemplate(template)); ArrayList<Path> out = new ArrayList<>();
Template ftl = this.cfg.getTemplate(template); for (Path template : templates) {
try (var outw = Files.newBufferedWriter(outpath, StandardCharsets.UTF_8)) { Path outpath = outputDir.resolve(outputFilenameFromTemplate(template.toString()));
ftl.process(this.data, outw); Files.createDirectories(outpath.getParent());
out.add(outpath); Path templateSubdirectory = template.getParent();
} ArrayList<String> templateDirectories= new ArrayList<>() ;
} if (templateSubdirectory != null) {
return out; 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) { private String outputFilenameFromTemplate(String template) {
return template.substring(0, template.length() - TEMPLATE_EXTENSION_LENGTH); return template.substring(0, template.length() - TEMPLATE_EXTENSION_LENGTH);
} }
} }

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,5 @@
package nu.zoom.dsl.run;
public interface Logger {
void println(String message);
}

View file

@ -0,0 +1,8 @@
package nu.zoom.dsl.run;
public class NullLogger implements Logger {
@Override
public void println(String message) {
}
}

View file

@ -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);
}
}

View file

@ -11,8 +11,11 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 nu.zoom.dsl.parser.*;
import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.CommonTokenStream;

View file

@ -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<Path> templates,
Optional<Path> output,
Optional<ParserType> 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<Path> 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
}
}

View file

@ -0,0 +1,9 @@
package nu.zoom.dsl.run;
public class StdoutLogger implements Logger {
@Override
public void println(String message) {
System.out.println(message);
}
}

View file

@ -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);
}
}

View file

@ -19,7 +19,7 @@
<groupId>nu.zoom.dsl</groupId> <groupId>nu.zoom.dsl</groupId>
<artifactId>endgen</artifactId> <artifactId>endgen</artifactId>
<version>1.1</version> <version>1.3-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<properties> <properties>
@ -59,12 +59,13 @@
<scm> <scm>
<connection>scm:git:https://codeberg.org/darkstar/endgen.git</connection> <connection>scm:git:https://codeberg.org/darkstar/endgen.git</connection>
<developerConnection>scm:git:ssh://git@vcs.zoom.nu:1122/zoom/endgen.git</developerConnection> <developerConnection>scm:git:ssh://git@vcs.zoom.nu:1122/zoom/endgen.git</developerConnection>
<tag>v1.1</tag> <tag>admin-parent-1.1</tag>
</scm> </scm>
<modules> <modules>
<module>parser</module> <module>parser</module>
<module>endgen-dist</module> <module>endgen-dist</module>
<module>endgen-maven-plugin</module>
</modules> </modules>
</project> </project>

70
sample-maven/pom.xml Normal file
View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
// Copyright 2025 "Johan Maasing" <johan@zoom.nu>
//
// 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>nu.zoom.dsl</groupId>
<artifactId>sample-maven</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<licenses>
<license>
<name>ASF 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
</license>
</licenses>
<developers>
<developer>
<name>Johan Maasing</name>
<email>johan@zoom.nu</email>
<roles>
<role>developer</role>
</roles>
</developer>
</developers>
<build>
<plugins>
<plugin>
<groupId>nu.zoom.dsl</groupId>
<artifactId>endgen-maven-plugin</artifactId>
<version>1.2-SNAPSHOT</version>
<executions>
<execution>
<goals>
<goal>endgen</goal>
</goals>
<configuration>
<templates>${project.basedir}/src/main/endpoint-templates</templates>
<dsl>${project.basedir}/src/main/endgen/test01.endpoints</dsl>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,28 @@
/*
Copyright 2025 "Johan Maasing" <johan@zoom.nu>
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<String>)
/yet/other/endpoint3 <- (bar2:Seq[AType]) -> (foo:Bar)

View file

@ -0,0 +1,21 @@
/*
Copyright 2025 "Johan Maasing" <johan@zoom.nu>
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

View file

@ -0,0 +1,22 @@
// Copyright 2025 "Johan Maasing" <johan@zoom.nu>
//
// 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>.</#items>;</#list>
class Endpoints() {
<#list endpoints as endpoint>
/* <#list endpoint.paths.paths><#items as segment>/${segment}</#items></#list> */
public void handle${endpoint.inputType?cap_first}
</#list>
}

View file

@ -16,6 +16,6 @@
{ title: SomeNodes, package: nu.zoom.dsl.states } { title: SomeNodes, package: nu.zoom.dsl.states }
start(s:S) -> middle(foo:foo): message(foo:foo), start(s:S) -> message(foo:foo) -> middle(foo:foo) ,
middle -> middle(bar:bar): selfmessage(bar:bar), middle -> selfmessage(bar:bar) -> middle(bar:bar),
middle -> end: message(bar:baz) middle -> message(bar:baz) -> end