Compare commits

..

15 commits
v1.0 ... main

45 changed files with 1133 additions and 137 deletions

2
.gitignore vendored
View file

@ -33,3 +33,5 @@ build/
### Mac OS ###
.DS_Store
/endpoints-output/**

163
README.md
View file

@ -32,27 +32,41 @@ parser and a code generator using [freemarker](https://freemarker.apache.org).
| mytemplate.xxx.ftl |
\____________________\
```
Endgen currently contains two separate parsers:
* endpoint - A DSL for expressing HTTP endpoints.
* state - A DSL for expressing state and transitions.
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
,see below for details.
## How to Run
You need a Java 24 runtime and java in the path. A very convenient way to install a java runtime is [SdkMan](https://sdkman.io).
You need a Java 21 (or later) runtime and java in the path. A very convenient way to install a java runtime is [SdkMan](https://sdkman.io).
Unpack the archive, run the provided shellscript file.
### Usage
```
Usage: run.sh [-hvV] [-o=<outputDir>] [-t=<templateDir>] <file>
sage: run.sh [-hvV] [-o=<outputDir>] [-p=<parser>] [-t=<templateDir>] <file>
Generate source code from an endpoints specification file.
<file> The source endpoints DSL file.
-h, --help Show this help message and exit.
-o, --output=<outputDir> The directory to write the generated code to.
Default is ~/endpoints-output
Default is endpoints-output
-p, --parser=<parser> Force use of a specific parser instead of
determining from filename. Valid values:
Endpoints, States.
-t, --template=<templateDir>
The template directory. Default is
~/endpoints-templates
endpoints-template
-v, --verbose Print verbose debug messages.
-V, --version Print version information and exit.
```
## DSL example
## Endpoint DSL example
In the simplest form the DSL looks like this
```
/some/endpoint <- SomeType(foo:String)
@ -75,13 +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)* ;
```
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.
the corresponding grammar for the root of the State-DSL
```antlrv4
document : generatorconfig? transition (',' transition)* ;
```
### Configuration block
Both types of DSL files has an optional `generatorconfig` block at the top.
Here is an example:
```
@ -89,28 +110,37 @@ 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 declaration. 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 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.
### Automatically named data types
### Automatically named endpoint data types
`/some/other/endpoint <- (bar:Seq[Embedded])` is another endpoint declaration. However this time the request body is
not named in the DSL. But all datatypes must have a name so it will simply name it after the last path segment and
tack on the string 'Request' at the end. So the AST till contain a datatype named `endpointRequest` with a field named
@ -122,7 +152,7 @@ decide to generate in the templates.
The only 'semantic' validation the parser performs is to check that not two types have the same name.
### Reponse data types
### Endpoint reponse data type
It is possible to have an optional response data type declared like so:
`/some/other/endpoint <- (bar:Seq[Embedded]) -> ResponseType(foo: Bar)`
@ -130,22 +160,63 @@ 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.
### DSL config
The only key in the config block the generator looks at is called `ending`, this will be used as the file ending for
the resulting file of applying the freemarker template.
### State DSL
This is an example of a state file:
```
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'.
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 -> message -> middle,
middle -> selfmessage -> middle(bar:bar),
middle -> message -> end
```
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) -> message(foo:foo) -> middle(foo:foo) ,
middle -> selfmessage(bar:bar) -> middle(bar:bar),
middle -> message(bar:baz) -> end
```
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 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(
public record GeneratorNode(
Map<String, String> config,
List<TypeNode> typeDefinitions,
List<EndpointNode> endpoints) {
Set<TypeNode> typeDefinitions,
List<EndpointNode> endpoints,
Set<StateNode> states,
Meta meta) {
}
```
This will be passed to the freemarker engine as the 'root' data object, meaning you have access to the parts in your freemarker template like this:
Depending on the parser used the endpoints or the states will be null but config and typeDefinitions are populated the
same for both parsers.
This will be passed to the freemarker engine as the 'root' data object, meaning you have access to the parts in your
freemarker template like this:
```injectedfreemarker
<#list typeDefinitions as type>
@ -153,7 +224,7 @@ This will be passed to the freemarker engine as the 'root' data object, meaning
</#list>
```
That is, you can directly reference `typeDefinitions`, `endpoints` or `config`.
That is, you can directly reference `typeDefinitions`, `endpoints`, `states` or `config`.
### Config
The config object is simply a String-map with the keys and values unfiltered from the input file. Here is an example
@ -162,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<FieldNode> fields) { }
@ -213,3 +284,43 @@ Output data type.
</#list>
```
### States
The set of states will hold items of this shape:
```injectedfreemarker
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.
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<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>
<groupId>nu.zoom.dsl</groupId>
<artifactId>endgen</artifactId>
<version>1.1-SNAPSHOT</version>
<version>1.3-SNAPSHOT</version>
</parent>
<artifactId>endgen-dist</artifactId>

View file

@ -1,4 +1,4 @@
#! /bin/sh
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
cd "${SCRIPT_DIR}"
java -jar parser-${artifact.baseVersion}.jar
java -jar parser-${artifact.baseVersion}.jar "$@"

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 endpoint.paths.paths>
<#items as segment>/${segment}</#items>

View file

@ -0,0 +1,19 @@
// 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>
object Codecs:
<#list typeDefinitions as type>
given Codec[${type.name?cap_first}] = deriveCodec
</#list>

View file

@ -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>.</#items>;</#list>
class Endpoints:
<#list endpoints as endpoint>

View file

@ -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>.</#items>;</#list>
object Protocol:
<#list typeDefinitions?sort as type>

View file

@ -20,7 +20,7 @@
<parent>
<groupId>nu.zoom.dsl</groupId>
<artifactId>endgen</artifactId>
<version>1.1-SNAPSHOT</version>
<version>1.3-SNAPSHOT</version>
</parent>
<artifactId>parser</artifactId>
@ -50,13 +50,14 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>24</source>
<target>24</target>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifest>

View file

@ -0,0 +1,41 @@
// 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.
grammar Common;
generatorconfig : '{' (configitem)? (',' configitem)* '}';
configitem : configkey ':' configvalue ;
configkey : IDENTIFIER ;
configvalue : (IDENTIFIER|VALUE) ;
namedTypeDeclaration : typeName typeDeclaration ;
typeName : IDENTIFIER ;
typeDeclaration : '(' typeField (',' typeField)* ')' ;
typeField : fieldName COLON fieldType ;
fieldName : IDENTIFIER ;
fieldType : IDENTIFIER ;
fragment LOWERCASE : [a-z] ;
fragment UPPERCASE : [A-Z] ;
fragment GENERICS : '['|']'|'<'|'>' ;
fragment DOT : '.' ;
fragment COMMENT_BEGIN : '/*' ;
fragment COMMENT_END : '*/' ;
fragment DIGIT : [0-9] ;
WS : [ \t\n\r]+ -> skip;
COMMENT : COMMENT_BEGIN .*? COMMENT_END -> skip;
LEFT_ARROW : '<-' ;
RIGHT_ARROW : '->' ;
IDENTIFIER : (LOWERCASE | UPPERCASE) (LOWERCASE | UPPERCASE | DIGIT | GENERICS | DOT)* ;
VALUE : ~[ ,{}:()/="#';*\n\r\t]+ ;
SLASH : '/' ;
COLON : ':' ;

View file

@ -12,35 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
grammar Endpoints;
import Common;
document : generatorconfig? (namedTypeDeclaration|endpoint)* ;
generatorconfig : '{' (configitem)? (',' configitem)* '}';
configitem : configkey ':' configvalue ;
configkey : IDENTIFIER ;
configvalue : (IDENTIFIER|VALUE) ;
namedTypeDeclaration : typeName typeDeclaration ;
typeName : IDENTIFIER ;
typeDeclaration : '(' typeField (',' typeField)* ')' ;
typeField : fieldName ':' fieldType ;
fieldName : IDENTIFIER ;
fieldType : IDENTIFIER ;
requestBody : REQUEST_PREFIX (namedTypeDeclaration | typeDeclaration | IDENTIFIER) ;
responseBody : RESPONSE_PREFIX (namedTypeDeclaration | typeDeclaration | IDENTIFIER) ;
requestBody : LEFT_ARROW (namedTypeDeclaration | typeDeclaration | IDENTIFIER) ;
responseBody : RIGHT_ARROW (namedTypeDeclaration | typeDeclaration | IDENTIFIER) ;
endpoint : path requestBody responseBody?;
path : (pathSegment)+ ;
pathSegment : SLASH (IDENTIFIER|VALUE) ;
fragment DIGIT : [0-9] ;
fragment LOWERCASE : [a-z] ;
fragment UPPERCASE : [A-Z] ;
fragment GENERICS : '['|']'|'<'|'>' ;
fragment DOT : '.' ;
fragment COMMENT_BEGIN : '/*' ;
fragment COMMENT_END : '*/' ;
WS : [ \t\n\r]+ -> skip;
COMMENT : COMMENT_BEGIN .*? COMMENT_END -> skip;
REQUEST_PREFIX : '<-' ;
RESPONSE_PREFIX : '->' ;
SLASH : '/' ;
IDENTIFIER : (LOWERCASE | UPPERCASE) (LOWERCASE | UPPERCASE | DIGIT | GENERICS | DOT)* ;
VALUE : ~[ ,{}:()/="#';*\n\r\t]+ ;

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.
grammar States;
import Common;
document : generatorconfig? transition (',' transition)* ;
transition : from RIGHT_ARROW message RIGHT_ARROW to ;
from : state ;
to : state ;
message : typeName typeDeclaration? ;
state : typeName typeDeclaration? ;

View file

@ -13,11 +13,14 @@
// 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;
public record DocumentNode(
Map<String, String> config,
List<TypeNode> typeDefinitions,
List<EndpointNode> endpoints) {
Set<TypeNode> typeDefinitions,
List<EndpointNode> endpoints,
Set<StateNode> states) {
}

View file

@ -19,7 +19,8 @@ import org.antlr.v4.runtime.tree.TerminalNode;
import java.util.*;
public class EndpointsVisitorTransformer extends EndpointsBaseVisitor<EndpointsParser.DocumentContext> {
public class EndpointsVisitorTransformer
extends EndpointsBaseVisitor<EndpointsParser.DocumentContext> {
private final ArrayList<EndpointNode> endpoints = new ArrayList<>();
private final HashMap<String,String> config = new HashMap<>();
private final HashSet<TypeNode> dataTypes = new HashSet<>();
@ -35,8 +36,8 @@ public class EndpointsVisitorTransformer extends EndpointsBaseVisitor<EndpointsP
return Map.copyOf(config);
}
public List<TypeNode> getDataTypes() {
return List.copyOf(dataTypes);
public Set<TypeNode> getDataTypes() {
return Set.copyOf(dataTypes);
}
@Override
@ -130,7 +131,7 @@ public class EndpointsVisitorTransformer extends EndpointsBaseVisitor<EndpointsP
).toList();
}
// Concatenate the text from to terminal nodes. Useful for contexts that are either an identifier or a value,
// Concatenate the text from two terminal nodes. Useful for contexts that are either an identifier or a value,
// and you just want the text from whichever is not null.
private String getText(TerminalNode identifier, TerminalNode value) {
return

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

@ -0,0 +1,6 @@
package nu.zoom.dsl.ast;
import java.util.Set;
public record StateNode(String name, String data, Set<TransitionNode> transitions) {
}

View file

@ -0,0 +1,132 @@
package nu.zoom.dsl.ast;
import nu.zoom.dsl.parser.StatesBaseVisitor;
import nu.zoom.dsl.parser.StatesParser;
import org.antlr.v4.runtime.tree.TerminalNode;
import java.util.*;
import java.util.stream.Stream;
public class StatesVisitorTransformer extends StatesBaseVisitor<StatesParser.DocumentContext> {
private final HashMap<String,String> config = new HashMap<>();
private final HashSet<TypeNode> nodeTypes = new HashSet<>();
private final HashSet<TypeNode> messageTypes = new HashSet<>();
// from -> <to, message>
private final HashMap<String,HashMap<String, String>> transitions = new HashMap<>();
@Override
public StatesParser.DocumentContext visitTransition(StatesParser.TransitionContext ctx) {
String from = ctx.from().state().typeName().IDENTIFIER().getText() ;
String to = ctx.to().state().typeName().IDENTIFIER().getText() ;
String message = ctx.message().typeName().IDENTIFIER().getText() ;
this.transitions.computeIfAbsent(from, k -> new HashMap<>()).put(to, message);
return super.visitTransition(ctx);
}
@Override
public StatesParser.DocumentContext visitState(StatesParser.StateContext ctx) {
String stateName = ctx.typeName().IDENTIFIER().getText() ;
List<FieldNode> fields = extractFields(ctx.typeDeclaration()) ;
this.nodeTypes.add(new TypeNode(stateName, fields));
return super.visitState(ctx);
}
@Override
public StatesParser.DocumentContext visitMessage(StatesParser.MessageContext ctx) {
String messageName = ctx.typeName().IDENTIFIER().getText() ;
List<FieldNode> fields = extractFields(ctx.typeDeclaration()) ;
this.messageTypes.add(new TypeNode(messageName, fields));
return super.visitMessage(ctx);
}
@Override
public StatesParser.DocumentContext visitConfigitem(StatesParser.ConfigitemContext ctx) {
String configKey = ctx.configkey().IDENTIFIER().getText();
String configValue = getText(ctx.configvalue().IDENTIFIER(), ctx.configvalue().VALUE());
this.config.put(configKey, configValue);
return super.visitConfigitem(ctx);
}
public Set<StateNode> getStates() {
HashSet<StateNode> states = new HashSet<>();
this.transitions.forEach((state,v)->{
HashSet<TransitionNode> transitionNodes = new HashSet<>();
v.forEach((to, message) -> transitionNodes.add(new TransitionNode(message, to)));
states.add(new StateNode(state, "", transitionNodes)) ;
}) ;
return states ;
}
public Map<String,String> getConfig() {
return Map.copyOf(config);
}
public Set<TypeNode> getTypes() {
final HashMap<String, TypeNode> stateTypeNodes = new HashMap<>();
this.nodeTypes.forEach(typeNode -> {
if (stateTypeNodes.containsKey(typeNode.name())) {
TypeNode mergedNode = mergeTypeFields(typeNode, stateTypeNodes.get(typeNode.name()));
stateTypeNodes.put(typeNode.name(), mergedNode);
} else {
stateTypeNodes.put(typeNode.name(), typeNode);
}
}) ;
final HashMap<String, TypeNode> messageTypeNodes = new HashMap<>();
this.messageTypes.forEach(typeNode -> {
if (stateTypeNodes.containsKey(typeNode.name())) {
throw new ParseException("Message " + typeNode.name() + " conflicts with state with the same type name");
}
if (messageTypeNodes.containsKey(typeNode.name())) {
TypeNode mergedNode = mergeTypeFields(typeNode, messageTypeNodes.get(typeNode.name()));
messageTypeNodes.put(typeNode.name(), mergedNode);
} else {
messageTypeNodes.put(typeNode.name(), typeNode);
}
}) ;
HashSet<TypeNode> allTypeNodes = new HashSet<>(stateTypeNodes.values());
allTypeNodes.addAll(messageTypeNodes.values());
return allTypeNodes ;
}
private TypeNode mergeTypeFields(TypeNode t1, TypeNode t2) {
List<FieldNode> t1Fields = (t1 != null) ? t1.fields() : List.of() ;
List<FieldNode> t2Fields = (t2 != null) ? t2.fields() : List.of() ;
HashMap<String, FieldNode> mergedFields = new HashMap<>();
t1Fields.forEach(field -> {
if (mergedFields.containsKey(field.name())) {
throw new ParseException("Duplicate field name: " + field.name());
}
mergedFields.put(field.name(), field);
});
t2Fields.forEach(field -> {
if (mergedFields.containsKey(field.name())) {
throw new ParseException("Duplicate field name: " + field.name());
}
mergedFields.put(field.name(), field);
});
return new TypeNode(t1.name(), mergedFields.values().stream().toList()) ;
}
private List<FieldNode> extractFields(StatesParser.TypeDeclarationContext declaration) {
if (declaration == null) {
return Collections.emptyList();
}
return declaration
.typeField()
.stream()
.map(
ctx ->
new FieldNode(ctx.fieldName().getText(), ctx.fieldType().getText())
)
.toList();
}
// Concatenate the text from two terminal nodes. Useful for contexts that are either an identifier or a value,
// and you just want the text from whichever is not null.
private String getText(TerminalNode identifier, TerminalNode value) {
return
((identifier != null) ? identifier.getText() : "") +
((value != null) ? value.getText() : "");
}
}

View file

@ -0,0 +1,4 @@
package nu.zoom.dsl.ast;
public record TransitionNode(String message, String toState) {
}

View file

@ -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(
@ -39,17 +39,20 @@ public class EndpointsCLI implements Callable<Integer> {
private Path file;
@SuppressWarnings("CanBeFinal")
@Option(names = {"-t", "--template"}, description = "The template directory. Default is ~/endpoints-templates")
private Path templateDir = Paths.get(System.getProperty("user.dir"), "endpoints-templates");
@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"}, description = "The directory to write the generated code to. Default is ~/endpoints-output")
private Path outputDir = Paths.get(System.getProperty("user.dir"), "endpoints-output");
@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")
@Option(names = {"-v", "--verbose"}, description = "Print verbose debug messages.")
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 Runner.ParserType parser = null;
public static void main(String[] args) {
int exitCode = new CommandLine(new EndpointsCLI()).execute(args);
System.exit(exitCode);
@ -58,15 +61,14 @@ public class EndpointsCLI implements Callable<Integer> {
@Override
public Integer call() {
try {
validateTemplateDirectory();
validateInputFile();
validateOutputDirectory();
DocumentNode rootNode = ParserWrapper.parse(file);
Generator generator = new Generator(templateDir, rootNode, outputDir);
List<Path> generatedPaths = generator.generate();
if (generatedPaths.isEmpty()) {
System.out.println("No generated paths found.");
}
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());
@ -74,6 +76,12 @@ public class EndpointsCLI implements Callable<Integer> {
}
}
private void verbose(String message) {
if (this.verbose) {
System.out.println(message);
}
}
private void validateOutputDirectory() throws IOException {
if (Files.notExists(this.outputDir)) {
Files.createDirectories(this.outputDir);

View file

@ -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,19 +28,20 @@ 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 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 {
public Generator(Path templatesDir, DocumentNode documentNode, Path outputDir) throws IOException {
this.templatesDir = Objects.requireNonNull(templatesDir);
this.data = Objects.requireNonNull(data);
this.documentNode = Objects.requireNonNull(documentNode);
this.outputDir = Objects.requireNonNull(outputDir);
this.cfg = new Configuration(Configuration.VERSION_2_3_34);
cfg.setDirectoryForTemplateLoading(templatesDir.toFile());
@ -50,19 +53,39 @@ public class Generator {
}
public List<Path> generate() throws IOException, TemplateException {
try (Stream<Path> files = Files.list(templatesDir)) {
List<String> templates = files
.map(Path::getFileName)
.map(Path::toString)
.filter(p -> p.length() > TEMPLATE_EXTENSION_LENGTH && p.endsWith(TEMPLATE_EXTENSION)
try (Stream<Path> files = Files.walk(templatesDir)) {
List<Path> 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<Path> out = new ArrayList<>();
for (String template : templates) {
Path outpath = outputDir.resolve(outputFilenameFromTemplate(template));
Template ftl = this.cfg.getTemplate(template);
for (Path template : templates) {
Path outpath = outputDir.resolve(outputFilenameFromTemplate(template.toString()));
Files.createDirectories(outpath.getParent());
Path templateSubdirectory = template.getParent();
ArrayList<String> 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(this.data, outw);
ftl.process(generatorNode, outw);
out.add(outpath);
}
}

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,25 +11,48 @@
// 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.parser.EndpointsLexer;
import nu.zoom.dsl.parser.EndpointsParser;
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;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
public class ParserWrapper {
public static DocumentNode parse(Path sourcePath) throws IOException {
public static DocumentNode parseEndpoints(Path sourcePath) throws IOException {
var ins = CharStreams.fromPath(sourcePath, StandardCharsets.UTF_8);
EndpointsLexer lexer = new EndpointsLexer(ins);
EndpointsParser parser = new EndpointsParser(new CommonTokenStream(lexer));
var document = parser.document();
var astTransformer = new EndpointsVisitorTransformer();
astTransformer.visit(document);
return new DocumentNode(astTransformer.getConfig(), astTransformer.getDataTypes(), astTransformer.getEndpoints());
return new DocumentNode(
astTransformer.getConfig(),
astTransformer.getDataTypes(),
astTransformer.getEndpoints(),
Set.of()
);
}
public static DocumentNode parseStates(Path sourcePath) throws IOException {
var ins = CharStreams.fromPath(sourcePath, StandardCharsets.UTF_8);
StatesLexer lexer = new StatesLexer(ins);
StatesParser parser = new StatesParser(new CommonTokenStream(lexer));
var document = parser.document();
var astTransformer = new StatesVisitorTransformer();
astTransformer.visit(document);
return new DocumentNode(
astTransformer.getConfig(),
astTransformer.getTypes(),
List.of(),
astTransformer.getStates()
);
}
}

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,12 +19,12 @@
<groupId>nu.zoom.dsl</groupId>
<artifactId>endgen</artifactId>
<version>1.1-SNAPSHOT</version>
<version>1.3-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>24</maven.compiler.source>
<maven.compiler.target>24</maven.compiler.target>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
@ -65,6 +65,7 @@
<modules>
<module>parser</module>
<module>endgen-dist</module>
<module>endgen-maven-plugin</module>
</modules>
</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

@ -0,0 +1,23 @@
// 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.
package ${config.package}
object StateAndMessageTypes:
<#list typeDefinitions?sort as type>
case class ${type.name?cap_first}(
<#list type.fields as field>
${field.name} : ${field.type},
</#list>
)
</#list>

View file

@ -0,0 +1,29 @@
```
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.
```
# The nodes
```mermaid
---
title: ${config.title}
---
stateDiagram-v2
<#list states as state>
<#list state.transitions as transition>
${state.name} --> ${transition.toState} : ${transition.message}
</#list>
</#list>
```

View file

@ -17,8 +17,7 @@
{
some: configvalue,
someother: value,
package: se.rutdev.senash,
ending: .scala
package: se.rutdev.senash
}
/some/endpoint <- SomeType(foo:String)

21
test01.states Normal file
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