diff --git a/endpoints.tapir b/endpoints.tapir index e69de29..108382e 100644 --- a/endpoints.tapir +++ b/endpoints.tapir @@ -0,0 +1,9 @@ +projekt/create/ -> createProjekt( + id: ProjektId, + properties: ProjektProperties +) + +projekt/update/ -> updateProjekt( + id: ProjektId, + properties: ProjektProperties +) \ No newline at end of file diff --git a/parser/pom.xml b/parser/pom.xml index 5062a5f..e5601dc 100755 --- a/parser/pom.xml +++ b/parser/pom.xml @@ -53,5 +53,9 @@ info.picocli picocli + + org.freemarker + freemarker + diff --git a/parser/src/main/java/nu/zoom/tapir/Generator.java b/parser/src/main/java/nu/zoom/tapir/Generator.java index 2282e1d..c48d20d 100755 --- a/parser/src/main/java/nu/zoom/tapir/Generator.java +++ b/parser/src/main/java/nu/zoom/tapir/Generator.java @@ -1,70 +1,99 @@ -package nu.zoom.tapir; - -import nu.zoom.tapir.parser.*; - -import java.io.IOException; -import java.io.StringReader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.concurrent.Callable; - -import picocli.CommandLine; -import picocli.CommandLine.Command; -import picocli.CommandLine.Option; -import picocli.CommandLine.Parameters; - -@Command(name = "tapirgen", mixinStandardHelpOptions = true, description = "Generate source code from a tapir endpoint specification file.") -public class Generator implements Callable { - @Parameters(index = "0", description = "The source tapir file.") - private Path file; - - @Option(names = {"-t", "--template"}, description = "The template directory. Default is ~/tapir-templates") - private Path templateDir = Paths.get(System.getProperty("user.dir"), "tapir-templates"); - - @Option(names = {"-o", "--output"}, description = "The directory to write the gerenated code to. Default is ~/tapir-output") - private Path outputDir = Paths.get(System.getProperty("user.dir"), "tapir-output"); - - public static void main(String[] args) throws ParseException { - int exitCode = new CommandLine(new Generator()).execute(args); - System.exit(exitCode); - } - - @Override - public Integer call() { - try { - validateTemplateDirectory(); - validateInputFile(); - StringReader reader = new StringReader("foo/bar/baz/ -> fooHandler(id:String, name:NonEmptyString)foo/baz/->bazHandler(nothing:Any)"); - var rootNode = new TapirParser(reader).endpoints(); - var endpoints = NodeTransformer.transform(rootNode); - rootNode.dump(""); - endpoints.forEach(endpoint -> { - System.out.println(endpoint); - }); - return 0; - } catch (Exception e) { - System.err.println(e.getMessage()); - return 1; - } - } - - private void validateTemplateDirectory() throws IOException { - if (!Files.isDirectory(this.templateDir)) { - throw new IllegalArgumentException("Template directory '" + this.templateDir + "' does not exist."); - } - Path endpointTemplate = this.templateDir.resolve("endpoints.ftl") ; - if (Files.notExists(endpointTemplate)) { - throw new IllegalArgumentException("Can not find: '" + endpointTemplate + "'."); - } - } - - private void validateInputFile() throws IOException { - if (Files.notExists(this.file)) { - throw new IllegalArgumentException("Input file '" + this.file + "' does not exist."); - } - if (!Files.isReadable(this.file) || !Files.isRegularFile(this.file)) { - throw new IllegalArgumentException("Input file '" + this.file + "' is not a readable file."); - } - } -} +package nu.zoom.tapir; + +import nu.zoom.tapir.parser.*; + +import java.io.IOException; +import java.io.StringReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.Callable; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +@Command(name = "tapirgen", mixinStandardHelpOptions = true, description = "Generate source code from a tapir endpoint specification file.") +public class Generator implements Callable { + @Parameters(index = "0", description = "The source tapir file.") + private Path file; + + @Option(names = {"-t", "--template"}, description = "The template directory. Default is ~/tapir-templates") + private Path templateDir = Paths.get(System.getProperty("user.dir"), "tapir-templates"); + + @Option(names = {"-o", "--output"}, description = "The directory to write the gerenated code to. Default is ~/tapir-output") + private Path outputDir = Paths.get(System.getProperty("user.dir"), "tapir-output"); + + @Option(names = {"-v", "--verbose"}, description = "Print verbose debug messages.") + private Boolean verbose = false; + + public static void main(String[] args) throws ParseException { + int exitCode = new CommandLine(new Generator()).execute(args); + System.exit(exitCode); + } + + @Override + public Integer call() { + try { + validateTemplateDirectory(); + validateInputFile(); + validateOutputDirectory(); + var rootNode = new TapirParser(Files.newBufferedReader(this.file)).endpoints(); + if (this.verbose) { + System.out.println("====== Parse Tree ======"); + rootNode.dump(""); + } + var endpoints = NodeTransformer.transform(rootNode); + if (endpoints.isEmpty()) { + System.err.println("No tapir endpoints found."); + return 2; + } + if (this.verbose) { + System.out.println("\n====== AST ======"); + endpoints.forEach(endpoint -> { + System.out.println(endpoint); + }); + } + TargetGenerator targetGenerator = new TargetGenerator( + this.verbose, + this.outputDir, + this.templateDir, + endpoints + ); + targetGenerator.generate(); + return 0; + } catch (Exception e) { + System.err.println(e.getMessage()); + return 1; + } + } + + private void validateOutputDirectory() throws IOException { + if (Files.notExists(this.outputDir)) { + Files.createDirectories(this.outputDir); + } + if (!Files.isDirectory(this.outputDir)) { + throw new IllegalArgumentException("Output directory: '" + this.outputDir + " 'is not a directory."); + } + } + + private void validateTemplateDirectory() throws IOException { + if (!Files.isDirectory(this.templateDir)) { + throw new IllegalArgumentException("Template directory '" + this.templateDir + "' does not exist."); + } + Path endpointTemplate = this.templateDir.resolve(TargetGenerator.ENDPOINTS_TEMPLATE_NAME); + if (Files.notExists(endpointTemplate)) { + throw new IllegalArgumentException("Can not find: '" + endpointTemplate + "'."); + } + } + + private void validateInputFile() throws IOException { + if (Files.notExists(this.file)) { + throw new IllegalArgumentException("Input file '" + this.file + "' does not exist."); + } + if (!Files.isReadable(this.file) || !Files.isRegularFile(this.file)) { + throw new IllegalArgumentException("Input file '" + this.file + "' is not a readable file."); + } + } +} diff --git a/parser/src/main/java/nu/zoom/tapir/TargetGenerator.java b/parser/src/main/java/nu/zoom/tapir/TargetGenerator.java index f4bcfa8..7c95f5a 100644 --- a/parser/src/main/java/nu/zoom/tapir/TargetGenerator.java +++ b/parser/src/main/java/nu/zoom/tapir/TargetGenerator.java @@ -1,12 +1,24 @@ package nu.zoom.tapir; +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.TemplateExceptionHandler; + +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.HashMap; import java.util.List; import java.util.Objects; public class TargetGenerator { private final Path outputPath; private final Path templatePath; + private final boolean verbose; + public static String ENDPOINTS_TEMPLATE_NAME = "endpoints.ftl"; + private final List endpoints; public static class TargetGeneratorException extends Exception { public TargetGeneratorException(String message) { @@ -18,10 +30,12 @@ public class TargetGenerator { } public TargetGenerator( + final boolean verbose, Path outputPath, Path templatePath, List endpoints ) { + this.verbose = verbose; this.outputPath = Objects.requireNonNull( outputPath, "Output path is required" @@ -30,9 +44,26 @@ public class TargetGenerator { templatePath, "Template path is required" ); + this.endpoints = Objects.requireNonNull(endpoints) ; } - public void generate() throws TargetGeneratorException { - + public void generate() throws TargetGeneratorException, IOException, TemplateException { + Configuration cfg = new Configuration(Configuration.VERSION_2_3_34); + cfg.setDirectoryForTemplateLoading(this.templatePath.toFile()); + cfg.setDefaultEncoding("UTF-8"); + cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + cfg.setLogTemplateExceptions(false); + cfg.setWrapUncheckedExceptions(true); + cfg.setFallbackOnNullLoopVariable(false); + Template temp = cfg.getTemplate(ENDPOINTS_TEMPLATE_NAME); + try (var outputFile = Files.newBufferedWriter( + outputPath.resolve("endpoints.scala"), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING + )) { + HashMap> templateData = new HashMap<>(); + templateData.put("endpoints", endpoints); + temp.process(templateData, outputFile); + } } } diff --git a/parser/src/main/jjtree/tapir.jjt b/parser/src/main/jjtree/tapir.jjt index 83babd3..72355da 100755 --- a/parser/src/main/jjtree/tapir.jjt +++ b/parser/src/main/jjtree/tapir.jjt @@ -27,8 +27,9 @@ TOKEN : { | | | - | - | )+ > + | + | + | ()* > } void path() : diff --git a/pom.xml b/pom.xml index 0e81479..6d0f4a6 100755 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,11 @@ picocli 4.7.6 + + org.freemarker + freemarker + 2.3.34 + diff --git a/tapir-templates/endpoints.ftl b/tapir-templates/endpoints.ftl index e69de29..d0c8157 100644 --- a/tapir-templates/endpoints.ftl +++ b/tapir-templates/endpoints.ftl @@ -0,0 +1,37 @@ +package se.senashdev.projekt.api + +import se.rutdev.projekt.api.HttpProtocol.VersionedResponse +import se.rutdev.framework.json.circe.RutUtilsCodec +import se.rutdev.framework +import se.rutdev.framework.service.api.{OAuthUtils, RequestMeta, RutTapir} +import se.rutdev.pd.ProblemDetailProtocol.ProblemDetail +import sttp.tapir.Schema + +class Endpoints(override val config: OAuthUtils.OAuthConfig) extends framework.service.api.Endpoints with RutTapir with RutUtilsCodec: + type ApiEndpoint[I, O] = OAuthEndpoint[RequestMeta.OAuthRequestMeta, I, ProblemDetail, O] + + <#list endpoints as endpoint> + case class ${endpoint.handler.name?cap_first}( + <#list endpoint.handler.fields as field> + ${field.name} : ${field.type}, + + ) + + + <#list endpoints as endpoint> + given Codec[${endpoint.handler.name?cap_first}] = deriveCodec + + + <#list endpoints as endpoint> + val ${endpoint.handler.name}Endpoint = ApiEndpoint[${endpoint.handler.name?cap_first}, VersionedResponse] = + <#list endpoint.paths.paths> + apiV1Endpoint + .post + <#items as segment> + .in("${segment}") + + .post + .in(jsonBody[${endpoint.handler.name?cap_first}]) + .out(jsonBody[VersionedResponse]) + +