From 68dc70c17618b0feaaaebf3d9c258d25c9b29793 Mon Sep 17 00:00:00 2001
From: Johan Maasing <johan@zoom.nu>
Date: Fri, 2 May 2025 08:44:44 +0200
Subject: [PATCH 1/4] Makes it possible to put templates in subdirectories

---
 .../{ => nu/zoom/dsl}/Codecs.scala.ftl        |  0
 .../{ => nu/zoom/dsl}/Endpoints.scala.ftl     |  0
 .../{ => nu/zoom/dsl}/Protocol.scala.ftl      |  0
 .../nu/zoom/dsl/freemarker/Generator.java     | 24 +++++++++++--------
 4 files changed, 14 insertions(+), 10 deletions(-)
 rename endpoints-templates/{ => nu/zoom/dsl}/Codecs.scala.ftl (100%)
 rename endpoints-templates/{ => nu/zoom/dsl}/Endpoints.scala.ftl (100%)
 rename endpoints-templates/{ => nu/zoom/dsl}/Protocol.scala.ftl (100%)

diff --git a/endpoints-templates/Codecs.scala.ftl b/endpoints-templates/nu/zoom/dsl/Codecs.scala.ftl
similarity index 100%
rename from endpoints-templates/Codecs.scala.ftl
rename to endpoints-templates/nu/zoom/dsl/Codecs.scala.ftl
diff --git a/endpoints-templates/Endpoints.scala.ftl b/endpoints-templates/nu/zoom/dsl/Endpoints.scala.ftl
similarity index 100%
rename from endpoints-templates/Endpoints.scala.ftl
rename to endpoints-templates/nu/zoom/dsl/Endpoints.scala.ftl
diff --git a/endpoints-templates/Protocol.scala.ftl b/endpoints-templates/nu/zoom/dsl/Protocol.scala.ftl
similarity index 100%
rename from endpoints-templates/Protocol.scala.ftl
rename to endpoints-templates/nu/zoom/dsl/Protocol.scala.ftl
diff --git a/parser/src/main/java/nu/zoom/dsl/freemarker/Generator.java b/parser/src/main/java/nu/zoom/dsl/freemarker/Generator.java
index dcfa86a..1ac2cb0 100644
--- a/parser/src/main/java/nu/zoom/dsl/freemarker/Generator.java
+++ b/parser/src/main/java/nu/zoom/dsl/freemarker/Generator.java
@@ -21,6 +21,7 @@ import nu.zoom.dsl.ast.DocumentNode;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.FileVisitOption;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -50,17 +51,20 @@ 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)
-                    )
-                    .toList();
+        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());
+                Template ftl = this.cfg.getTemplate(template.toString());
                 try (var outw = Files.newBufferedWriter(outpath, StandardCharsets.UTF_8)) {
                     ftl.process(this.data, outw);
                     out.add(outpath);

From 7e8aa018e95541affd65b07b1065484f287d834d Mon Sep 17 00:00:00 2001
From: Johan Maasing <johan@zoom.nu>
Date: Fri, 2 May 2025 09:25:27 +0200
Subject: [PATCH 2/4] Makes it possible to put templates in subdirectories

---
 README.md                                     |  22 +++-
 endpoints-templates/endpoints.txt.ftl         |   2 +
 .../nu/zoom/dsl/Codecs.scala.ftl              |   2 +-
 .../nu/zoom/dsl/Endpoints.scala.ftl           |   2 +-
 .../nu/zoom/dsl/Protocol.scala.ftl            |   2 +-
 .../java/nu/zoom/dsl/ast/DocumentNode.java    |   1 +
 .../java/nu/zoom/dsl/ast/GeneratorNode.java   |  13 +++
 .../src/main/java/nu/zoom/dsl/ast/Meta.java   |   9 ++
 .../nu/zoom/dsl/freemarker/Generator.java     | 109 ++++++++++--------
 9 files changed, 109 insertions(+), 53 deletions(-)
 create mode 100644 parser/src/main/java/nu/zoom/dsl/ast/GeneratorNode.java
 create mode 100644 parser/src/main/java/nu/zoom/dsl/ast/Meta.java

diff --git a/README.md b/README.md
index d4306c2..7dc6cc1 100644
--- a/README.md
+++ b/README.md
@@ -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,14 @@ 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.
+It 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>
+```
\ No newline at end of file
diff --git a/endpoints-templates/endpoints.txt.ftl b/endpoints-templates/endpoints.txt.ftl
index 9a779fb..4904c00 100644
--- a/endpoints-templates/endpoints.txt.ftl
+++ b/endpoints-templates/endpoints.txt.ftl
@@ -1,3 +1,5 @@
+Generated from template: ${meta.templateFile}
+
 <#list endpoints as endpoint>
     <#list endpoint.paths.paths>
     <#items as segment>/${segment}</#items>
diff --git a/endpoints-templates/nu/zoom/dsl/Codecs.scala.ftl b/endpoints-templates/nu/zoom/dsl/Codecs.scala.ftl
index f11070f..7ea382d 100644
--- a/endpoints-templates/nu/zoom/dsl/Codecs.scala.ftl
+++ b/endpoints-templates/nu/zoom/dsl/Codecs.scala.ftl
@@ -11,7 +11,7 @@
 //   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 //   See the License for the specific language governing permissions and
 //   limitations under the License.
-package ${config.package}
+<#list meta.templateDirectories>package <#items as dir>${dir}<#sep>.</#items>;</#list>
 
 object Codecs:
 <#list typeDefinitions as type>
diff --git a/endpoints-templates/nu/zoom/dsl/Endpoints.scala.ftl b/endpoints-templates/nu/zoom/dsl/Endpoints.scala.ftl
index a68864c..fbc3538 100644
--- a/endpoints-templates/nu/zoom/dsl/Endpoints.scala.ftl
+++ b/endpoints-templates/nu/zoom/dsl/Endpoints.scala.ftl
@@ -11,7 +11,7 @@
 //   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 //   See the License for the specific language governing permissions and
 //   limitations under the License.
-package ${config.package}
+<#list meta.templateDirectories>package<#items as dir>${dir}<#sep>.</#items>;</#list>
 
 class Endpoints:
 <#list endpoints as endpoint>
diff --git a/endpoints-templates/nu/zoom/dsl/Protocol.scala.ftl b/endpoints-templates/nu/zoom/dsl/Protocol.scala.ftl
index faa5d55..41e6547 100644
--- a/endpoints-templates/nu/zoom/dsl/Protocol.scala.ftl
+++ b/endpoints-templates/nu/zoom/dsl/Protocol.scala.ftl
@@ -11,7 +11,7 @@
 //   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 //   See the License for the specific language governing permissions and
 //   limitations under the License.
-package ${config.package}
+<#list meta.templateDirectories>package<#items as dir>${dir}<#sep>.</#items>;</#list>
 
 object Protocol:
 <#list typeDefinitions?sort as type>
diff --git a/parser/src/main/java/nu/zoom/dsl/ast/DocumentNode.java b/parser/src/main/java/nu/zoom/dsl/ast/DocumentNode.java
index fcb8621..7edb239 100644
--- a/parser/src/main/java/nu/zoom/dsl/ast/DocumentNode.java
+++ b/parser/src/main/java/nu/zoom/dsl/ast/DocumentNode.java
@@ -13,6 +13,7 @@
 //   limitations under the License.
 package nu.zoom.dsl.ast;
 
+import java.nio.file.Path;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
diff --git a/parser/src/main/java/nu/zoom/dsl/ast/GeneratorNode.java b/parser/src/main/java/nu/zoom/dsl/ast/GeneratorNode.java
new file mode 100644
index 0000000..2cea7ca
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/ast/GeneratorNode.java
@@ -0,0 +1,13 @@
+package nu.zoom.dsl.ast;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public record GeneratorNode(
+	Map<String, String> config,
+	Set<TypeNode> typeDefinitions,
+	List<EndpointNode> endpoints,
+	Set<StateNode> states,
+	Meta meta) {
+}
diff --git a/parser/src/main/java/nu/zoom/dsl/ast/Meta.java b/parser/src/main/java/nu/zoom/dsl/ast/Meta.java
new file mode 100644
index 0000000..567f3ab
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/ast/Meta.java
@@ -0,0 +1,9 @@
+package nu.zoom.dsl.ast;
+
+import java.util.List;
+
+public record Meta(
+	List<String> templateDirectories,
+	String templateFile
+) {
+}
diff --git a/parser/src/main/java/nu/zoom/dsl/freemarker/Generator.java b/parser/src/main/java/nu/zoom/dsl/freemarker/Generator.java
index 1ac2cb0..0458669 100644
--- a/parser/src/main/java/nu/zoom/dsl/freemarker/Generator.java
+++ b/parser/src/main/java/nu/zoom/dsl/freemarker/Generator.java
@@ -18,63 +18,82 @@ 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;
-import java.nio.file.FileVisitOption;
 import java.nio.file.Files;
 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.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());
-                Template ftl = this.cfg.getTemplate(template.toString());
-                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);
+	}
 }

From e0922d5639f1f5a5e7a17b264439a10b92bfa9f4 Mon Sep 17 00:00:00 2001
From: Johan Maasing <johan@zoom.nu>
Date: Fri, 2 May 2025 09:25:27 +0200
Subject: [PATCH 3/4] Makes it possible to put templates in subdirectories

---
 README.md | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 7dc6cc1..3262cf7 100644
--- a/README.md
+++ b/README.md
@@ -309,10 +309,18 @@ public record TransitionNode(String message, String toState) {
 ### Meta
 
 The meta container holds information about the template file used to generate the output file.
-It holds the subdirectory below the given `templateDir` where the template was found.
+```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>
-```
\ No newline at end of file
+```
+
+* `templateFile` is the filename of the template (including the .ftl-ending) used to generate the output.
\ No newline at end of file

From 46da5c50195aeba0ddb71cb8625f8a6be1514984 Mon Sep 17 00:00:00 2001
From: Johan Maasing <johan@zoom.nu>
Date: Sun, 4 May 2025 09:41:16 +0200
Subject: [PATCH 4/4] needs testing

---
 endgen-maven-plugin/README.md                 |  8 ++
 endgen-maven-plugin/pom.xml                   | 69 +++++++++++++
 .../java/nu/zoom/dsl/maven/EndgenMojo.java    | 64 +++++++++++++
 .../java/nu/zoom/dsl/maven/MavenLogger.java   | 17 ++++
 .../src/main/java/nu/zoom/dsl/maven/Run.java  | 52 ++++++++++
 .../java/nu/zoom/dsl/cli/EndpointsCLI.java    | 48 +++-------
 .../java/nu/zoom/dsl/run/EndgenException.java | 15 +++
 .../nu/zoom/dsl/run/GeneratorException.java   | 15 +++
 .../src/main/java/nu/zoom/dsl/run/Logger.java |  5 +
 .../main/java/nu/zoom/dsl/run/NullLogger.java |  8 ++
 .../java/nu/zoom/dsl/run/ParserException.java | 15 +++
 .../zoom/dsl/{ast => run}/ParserWrapper.java  |  5 +-
 .../src/main/java/nu/zoom/dsl/run/Runner.java | 96 +++++++++++++++++++
 .../java/nu/zoom/dsl/run/StdoutLogger.java    |  9 ++
 .../nu/zoom/dsl/run/ValidationException.java  | 15 +++
 pom.xml                                       |  1 +
 16 files changed, 406 insertions(+), 36 deletions(-)
 create mode 100644 endgen-maven-plugin/README.md
 create mode 100644 endgen-maven-plugin/pom.xml
 create mode 100644 endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/EndgenMojo.java
 create mode 100644 endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/MavenLogger.java
 create mode 100644 endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/Run.java
 create mode 100644 parser/src/main/java/nu/zoom/dsl/run/EndgenException.java
 create mode 100644 parser/src/main/java/nu/zoom/dsl/run/GeneratorException.java
 create mode 100644 parser/src/main/java/nu/zoom/dsl/run/Logger.java
 create mode 100644 parser/src/main/java/nu/zoom/dsl/run/NullLogger.java
 create mode 100644 parser/src/main/java/nu/zoom/dsl/run/ParserException.java
 rename parser/src/main/java/nu/zoom/dsl/{ast => run}/ParserWrapper.java (93%)
 create mode 100644 parser/src/main/java/nu/zoom/dsl/run/Runner.java
 create mode 100644 parser/src/main/java/nu/zoom/dsl/run/StdoutLogger.java
 create mode 100644 parser/src/main/java/nu/zoom/dsl/run/ValidationException.java

diff --git a/endgen-maven-plugin/README.md b/endgen-maven-plugin/README.md
new file mode 100644
index 0000000..1e02921
--- /dev/null
+++ b/endgen-maven-plugin/README.md
@@ -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
\ No newline at end of file
diff --git a/endgen-maven-plugin/pom.xml b/endgen-maven-plugin/pom.xml
new file mode 100644
index 0000000..e9f42c5
--- /dev/null
+++ b/endgen-maven-plugin/pom.xml
@@ -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>
\ No newline at end of file
diff --git a/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/EndgenMojo.java b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/EndgenMojo.java
new file mode 100644
index 0000000..8d204aa
--- /dev/null
+++ b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/EndgenMojo.java
@@ -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);
+    }
+}
diff --git a/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/MavenLogger.java b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/MavenLogger.java
new file mode 100644
index 0000000..03b98be
--- /dev/null
+++ b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/MavenLogger.java
@@ -0,0 +1,17 @@
+package nu.zoom.dsl.maven;
+
+import nu.zoom.dsl.run.Logger;
+import org.apache.maven.plugin.logging.Log;
+
+public class MavenLogger implements Logger {
+    private final Log delegate;
+
+    public MavenLogger(Log delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public void println(String message) {
+        this.delegate.debug(message);
+    }
+}
diff --git a/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/Run.java b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/Run.java
new file mode 100644
index 0000000..c8a0779
--- /dev/null
+++ b/endgen-maven-plugin/src/main/java/nu/zoom/dsl/maven/Run.java
@@ -0,0 +1,52 @@
+package nu.zoom.dsl.maven;
+
+import java.io.File;
+
+public class Run {
+    private File templates;
+    private File output;
+    private File dsl;
+    private String parser;
+
+    public File getTemplates() {
+        return templates;
+    }
+
+    public void setTemplates(File templates) {
+        this.templates = templates;
+    }
+
+    public File getOutput() {
+        return output;
+    }
+
+    public void setOutput(File output) {
+        this.output = output;
+    }
+
+    public File getDsl() {
+        return dsl;
+    }
+
+    public void setDsl(File dsl) {
+        this.dsl = dsl;
+    }
+
+    public String getParser() {
+        return parser;
+    }
+
+    public void setParser(String parser) {
+        this.parser = parser;
+    }
+
+    @Override
+    public String toString() {
+        return "Run{" +
+                "templates=" + templates +
+                ", output=" + output +
+                ", dsl=" + dsl +
+                ", parser='" + parser + '\'' +
+                '}';
+    }
+}
diff --git a/parser/src/main/java/nu/zoom/dsl/cli/EndpointsCLI.java b/parser/src/main/java/nu/zoom/dsl/cli/EndpointsCLI.java
index e3d5794..8d79757 100644
--- a/parser/src/main/java/nu/zoom/dsl/cli/EndpointsCLI.java
+++ b/parser/src/main/java/nu/zoom/dsl/cli/EndpointsCLI.java
@@ -14,7 +14,7 @@
 package nu.zoom.dsl.cli;
 
 import nu.zoom.dsl.ast.DocumentNode;
-import nu.zoom.dsl.ast.ParserWrapper;
+import nu.zoom.dsl.run.*;
 import nu.zoom.dsl.freemarker.Generator;
 import picocli.CommandLine;
 import picocli.CommandLine.Command;
@@ -24,8 +24,8 @@ import picocli.CommandLine.Parameters;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.Callable;
 
 @Command(
@@ -34,20 +34,16 @@ import java.util.concurrent.Callable;
 	description = "Generate source code from an endpoints specification file."
 )
 public class EndpointsCLI implements Callable<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());
diff --git a/parser/src/main/java/nu/zoom/dsl/run/EndgenException.java b/parser/src/main/java/nu/zoom/dsl/run/EndgenException.java
new file mode 100644
index 0000000..7771e11
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/run/EndgenException.java
@@ -0,0 +1,15 @@
+package nu.zoom.dsl.run;
+
+public abstract class EndgenException extends Exception {
+    public EndgenException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public EndgenException(Throwable cause) {
+        super(cause);
+    }
+
+    public EndgenException(String message) {
+        super(message);
+    }
+}
diff --git a/parser/src/main/java/nu/zoom/dsl/run/GeneratorException.java b/parser/src/main/java/nu/zoom/dsl/run/GeneratorException.java
new file mode 100644
index 0000000..5157d97
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/run/GeneratorException.java
@@ -0,0 +1,15 @@
+package nu.zoom.dsl.run;
+
+public class GeneratorException extends EndgenException {
+    public GeneratorException(String message) {
+        super(message);
+    }
+
+    public GeneratorException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public GeneratorException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/parser/src/main/java/nu/zoom/dsl/run/Logger.java b/parser/src/main/java/nu/zoom/dsl/run/Logger.java
new file mode 100644
index 0000000..80826d3
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/run/Logger.java
@@ -0,0 +1,5 @@
+package nu.zoom.dsl.run;
+
+public interface Logger {
+    void println(String message);
+}
diff --git a/parser/src/main/java/nu/zoom/dsl/run/NullLogger.java b/parser/src/main/java/nu/zoom/dsl/run/NullLogger.java
new file mode 100644
index 0000000..014f473
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/run/NullLogger.java
@@ -0,0 +1,8 @@
+package nu.zoom.dsl.run;
+
+public class NullLogger implements Logger {
+    @Override
+    public void println(String message) {
+
+    }
+}
diff --git a/parser/src/main/java/nu/zoom/dsl/run/ParserException.java b/parser/src/main/java/nu/zoom/dsl/run/ParserException.java
new file mode 100644
index 0000000..b726384
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/run/ParserException.java
@@ -0,0 +1,15 @@
+package nu.zoom.dsl.run;
+
+public class ParserException extends EndgenException {
+    public ParserException(String message) {
+        super(message);
+    }
+
+    public ParserException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ParserException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/parser/src/main/java/nu/zoom/dsl/ast/ParserWrapper.java b/parser/src/main/java/nu/zoom/dsl/run/ParserWrapper.java
similarity index 93%
rename from parser/src/main/java/nu/zoom/dsl/ast/ParserWrapper.java
rename to parser/src/main/java/nu/zoom/dsl/run/ParserWrapper.java
index 11a95fd..ce62d9c 100644
--- a/parser/src/main/java/nu/zoom/dsl/ast/ParserWrapper.java
+++ b/parser/src/main/java/nu/zoom/dsl/run/ParserWrapper.java
@@ -11,8 +11,11 @@
 //   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 //   See the License for the specific language governing permissions and
 //   limitations under the License.
-package nu.zoom.dsl.ast;
+package nu.zoom.dsl.run;
 
+import nu.zoom.dsl.ast.DocumentNode;
+import nu.zoom.dsl.ast.EndpointsVisitorTransformer;
+import nu.zoom.dsl.ast.StatesVisitorTransformer;
 import nu.zoom.dsl.parser.*;
 import org.antlr.v4.runtime.CharStreams;
 import org.antlr.v4.runtime.CommonTokenStream;
diff --git a/parser/src/main/java/nu/zoom/dsl/run/Runner.java b/parser/src/main/java/nu/zoom/dsl/run/Runner.java
new file mode 100644
index 0000000..ebf162b
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/run/Runner.java
@@ -0,0 +1,96 @@
+package nu.zoom.dsl.run;
+
+import freemarker.template.TemplateException;
+import nu.zoom.dsl.ast.DocumentNode;
+import nu.zoom.dsl.freemarker.Generator;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Optional;
+
+public final class Runner {
+    public static final String DEFAULT_TEMPLATE_DIRECTORY_NAME = "endpoints-template";
+    public static final String DEFAULT_OUTPUT_DIRECTORY_NAME = "endpoints-output";
+
+    public static void run(
+            Path dsl,
+            Optional<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
+    }
+}
diff --git a/parser/src/main/java/nu/zoom/dsl/run/StdoutLogger.java b/parser/src/main/java/nu/zoom/dsl/run/StdoutLogger.java
new file mode 100644
index 0000000..7b0b7d4
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/run/StdoutLogger.java
@@ -0,0 +1,9 @@
+package nu.zoom.dsl.run;
+
+public class StdoutLogger implements Logger {
+
+    @Override
+    public void println(String message) {
+        System.out.println(message);
+    }
+}
diff --git a/parser/src/main/java/nu/zoom/dsl/run/ValidationException.java b/parser/src/main/java/nu/zoom/dsl/run/ValidationException.java
new file mode 100644
index 0000000..1627d72
--- /dev/null
+++ b/parser/src/main/java/nu/zoom/dsl/run/ValidationException.java
@@ -0,0 +1,15 @@
+package nu.zoom.dsl.run;
+
+public class ValidationException extends EndgenException {
+    public ValidationException(String message) {
+        super(message);
+    }
+
+    public ValidationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ValidationException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/pom.xml b/pom.xml
index 86744cc..cb060c8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -65,6 +65,7 @@
     <modules>
         <module>parser</module>
         <module>endgen-dist</module>
+        <module>endgen-maven-plugin</module>
     </modules>
 
 </project>