Merge pull request 'maven' (#1) from maven into main

Reviewed-on: #1
This commit is contained in:
Johan Maasing 2025-05-04 15:07:47 +02:00
commit 3175339bae
22 changed files with 548 additions and 39 deletions

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.2-SNAPSHOT</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

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

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

@ -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.2-SNAPSHOT</version> <version>1.3-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<properties> <properties>
@ -65,6 +65,7 @@
<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>
}