Compare commits

..

4 commits

25 changed files with 523 additions and 85 deletions

View file

@ -203,11 +203,12 @@ One restriction is that a state and a messages may share have the same name, i.e
If the parser is successful it will hold the following data in the AST
```java
public record DocumentNode(
Map<String, String> config,
Set<TypeNode> typeDefinitions,
List<EndpointNode> endpoints,
Set<StateNode> states) {
public record GeneratorNode(
Map<String, String> config,
Set<TypeNode> typeDefinitions,
List<EndpointNode> endpoints,
Set<StateNode> states,
Meta meta) {
}
```
@ -304,3 +305,22 @@ 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

@ -0,0 +1,8 @@
# References
## Maven
https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html#generatedsourcesdirectory
### For executing the plugin several times
See executions
https://maven.apache.org/guides/mini/guide-configuring-plugins.html

View file

@ -0,0 +1,69 @@
<?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.2-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>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-plugin-plugin</artifactId>
<version>${maven-plugin-tools.version}</version>
<executions>
<execution>
<id>help-mojo</id>
<goals>
<goal>helpmojo</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>nu.zoom.dsl</groupId>
<artifactId>endgen-maven-plugin</artifactId>
<version>1.2-SNAPSHOT</version>
<executions>
<execution>
<configuration>
<templates>${project.build.sourceDirectory}/main/endgen-templates</templates>
<output>${project.build.sourceDirectory}/generated-sources/endgen endpoints-output</output>
<dsl>${project.basedir}/../test01.endpoints</dsl>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

View file

@ -0,0 +1,64 @@
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(defaultValue = "${project.build.sourceDirectory}/main/endgen-templates")
File templates;
@Parameter(defaultValue = "${project.build.outputDirectory}/generated-sources/endgen")
File output;
@Parameter
File dsl;
@Parameter
String parser;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
getLog().info("Running endgen");
getLog().info("Using dsl: " + dsl);
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

@ -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 Codecs:
<#list typeDefinitions as type>

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

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

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;
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<Integer> {
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<Integer> {
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<Integer> {
@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<Path> 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());

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,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<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)
)
.toList();
ArrayList<Path> 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<Path> generate() throws IOException, TemplateException {
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 (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(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);
}
}

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.
// 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;

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

@ -65,6 +65,7 @@
<modules>
<module>parser</module>
<module>endgen-dist</module>
<module>endgen-maven-plugin</module>
</modules>
</project>