diff --git a/.gitignore b/.gitignore index 6efeca9..2b562cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +tapir-out/** # ---> Scala *.class *.log @@ -39,25 +40,6 @@ replay_pid* .idea/** *.iml -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using @@ -74,9 +56,6 @@ replay_pid* # CMake cmake-build-*/ -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - # File-based project format *.iws diff --git a/endpoints.tapir b/endpoints.tapir index 108382e..19eeef3 100644 --- a/endpoints.tapir +++ b/endpoints.tapir @@ -1,9 +1,14 @@ -projekt/create/ -> createProjekt( +ProjektProperties( + title: String, + description: String +) + +/createProject -> ( id: ProjektId, properties: ProjektProperties ) -projekt/update/ -> updateProjekt( +/updateProject -> ( id: ProjektId, properties: ProjektProperties ) \ No newline at end of file diff --git a/parser/pom.xml b/parser/pom.xml index e5601dc..b29ad05 100755 --- a/parser/pom.xml +++ b/parser/pom.xml @@ -32,6 +32,7 @@ --> jjtree-javacc + generate-sources jjtree-javacc diff --git a/parser/src/main/java/nu/zoom/tapir/DataTypeNode.java b/parser/src/main/java/nu/zoom/tapir/DataTypeNode.java new file mode 100644 index 0000000..923214e --- /dev/null +++ b/parser/src/main/java/nu/zoom/tapir/DataTypeNode.java @@ -0,0 +1,6 @@ +package nu.zoom.tapir; + +import java.util.List; + +public record DataTypeNode(String name, List fields) { +} diff --git a/parser/src/main/java/nu/zoom/tapir/Generator.java b/parser/src/main/java/nu/zoom/tapir/Generator.java index c48d20d..8488a2b 100755 --- a/parser/src/main/java/nu/zoom/tapir/Generator.java +++ b/parser/src/main/java/nu/zoom/tapir/Generator.java @@ -39,19 +39,25 @@ public class Generator implements Callable { validateTemplateDirectory(); validateInputFile(); validateOutputDirectory(); - var rootNode = new TapirParser(Files.newBufferedReader(this.file)).endpoints(); + var rootNode = new TapirParser(Files.newBufferedReader(this.file)).specification(); if (this.verbose) { System.out.println("====== Parse Tree ======"); rootNode.dump(""); } - var endpoints = NodeTransformer.transform(rootNode); - if (endpoints.isEmpty()) { + NodeTransformer transformer = new NodeTransformer(); + transformer.transform(rootNode); + if (transformer.getEndpoints().isEmpty()) { System.err.println("No tapir endpoints found."); return 2; } if (this.verbose) { System.out.println("\n====== AST ======"); - endpoints.forEach(endpoint -> { + System.out.println("\n====== Types ======"); + transformer.getDataTypes().forEach(type -> { + System.out.println(type); + }); + System.out.println("\n====== Endpoints ======"); + transformer.getEndpoints().forEach(endpoint -> { System.out.println(endpoint); }); } @@ -59,7 +65,8 @@ public class Generator implements Callable { this.verbose, this.outputDir, this.templateDir, - endpoints + transformer.getEndpoints(), + transformer.getDataTypes() ); targetGenerator.generate(); return 0; diff --git a/parser/src/main/java/nu/zoom/tapir/NodeTransformer.java b/parser/src/main/java/nu/zoom/tapir/NodeTransformer.java index c4ed179..942465d 100644 --- a/parser/src/main/java/nu/zoom/tapir/NodeTransformer.java +++ b/parser/src/main/java/nu/zoom/tapir/NodeTransformer.java @@ -9,14 +9,95 @@ import java.util.ArrayList; import java.util.List; public class NodeTransformer { + private final List endpoints = new ArrayList<>(); + private final List dataTypes = new ArrayList<>(); - public static List transform(SimpleNode rootNode) throws ParseException { - ArrayList endpoints = new ArrayList<>(); - for (int i = 0; i < rootNode.jjtGetNumChildren(); i++) { - SimpleNode endpoint = assertSimpleNode(rootNode.jjtGetChild(i)); - endpoints.add(handleEndpoint(endpoint)); + public List getEndpoints() { + return endpoints; + } + + public List getDataTypes() { + return dataTypes; + } + + public void transform(SimpleNode rootNode) throws ParseException { + assertSimpleNodeType(rootNode, TapirParserTreeConstants.JJTSPECIFICATION); + int numChildren = rootNode.jjtGetNumChildren(); + if (numChildren == 2) { + this.dataTypes.addAll( + handleDataTypes( + assertSimpleNodeType( + rootNode.jjtGetChild(0), + TapirParserTreeConstants.JJTDATATYPES + ) + ) + ); + this.endpoints.addAll( + handleEndpoints( + assertSimpleNodeType( + rootNode.jjtGetChild(1), + TapirParserTreeConstants.JJTENDPOINTS + ) + ) + ); + } else if (numChildren == 1) { + this.endpoints.addAll( + handleEndpoints( + assertSimpleNodeType( + rootNode.jjtGetChild(1), + TapirParserTreeConstants.JJTENDPOINTS + ) + ) + ); + } else { + throw new ParseException("Expected specification to have 1 or 2 children but had " + numChildren); } - return endpoints ; + } + + private static List handleEndpoints(SimpleNode endpoints) throws ParseException { + ArrayList endpointNodes = new ArrayList<>(); + for (int i = 0; i < endpoints.jjtGetNumChildren(); i++) { + endpointNodes.add( + handleEndpoint( + assertSimpleNodeType( + endpoints.jjtGetChild(i), + TapirParserTreeConstants.JJTENDPOINT + ) + ) + ); + } + return endpointNodes; + } + + private static List handleDataTypes(SimpleNode dataTypesDeclaration) throws ParseException { + List dataTypes = new ArrayList<>(); + for (int i = 0; i < dataTypesDeclaration.jjtGetNumChildren(); i++) { + dataTypes.add( + handleCompoundDataType( + assertSimpleNodeType( + dataTypesDeclaration.jjtGetChild(i), + TapirParserTreeConstants.JJTCOMPOUNDDATATYPE + ) + ) + ); + } + return dataTypes; + } + + private static DataTypeNode handleCompoundDataType(SimpleNode dataTypeNode) throws ParseException { + String typename = getStringValue( + assertSimpleNodeType( + dataTypeNode.jjtGetChild(0), + TapirParserTreeConstants.JJTCOMPUNDDATATYPENAME + ) + ); + List fields = handleFields( + assertSimpleNodeType( + dataTypeNode.jjtGetChild(1), + TapirParserTreeConstants.JJTDATATYPEFIELDS + ) + ); + return new DataTypeNode(typename, fields); } private static EndpointNode handleEndpoint(SimpleNode node) throws ParseException { @@ -27,15 +108,16 @@ public class NodeTransformer { SimpleNode pathsParseNode = assertSimpleNodeType( node.jjtGetChild(0), - TapirParserTreeConstants.JJTPATHS + TapirParserTreeConstants.JJTPATH ); PathsNode pathsNode = handlePaths(pathsParseNode); SimpleNode handlerParseNode = assertSimpleNodeType( node.jjtGetChild(1), - TapirParserTreeConstants.JJTHANDLERSPEC + TapirParserTreeConstants.JJTDATATYPEFIELDS ); - HandlerNode handlerNode = handleHandler(handlerParseNode); + List fields = handleFields(handlerParseNode); + HandlerNode handlerNode = new HandlerNode(pathsNode.paths().getLast(), fields); return new EndpointNode(pathsNode, handlerNode); } @@ -47,25 +129,25 @@ public class NodeTransformer { String handlerName = getStringValue( assertSimpleNodeType( handlerSpec.jjtGetChild(0), - TapirParserTreeConstants.JJTHANDLERNAME + TapirParserTreeConstants.JJTCOMPUNDDATATYPENAME ) ); SimpleNode payloadFieldsParseNode = assertSimpleNodeType( handlerSpec.jjtGetChild(1), - TapirParserTreeConstants.JJTPAYLOADFIELDS + TapirParserTreeConstants.JJTDATATYPEFIELDS ); List fields = handleFields(payloadFieldsParseNode); return new HandlerNode(handlerName, fields); } - private static List handleFields(SimpleNode payloadFieldsParseNode) throws ParseException { + private static List handleFields(SimpleNode compoundDatatTypeFields) throws ParseException { ArrayList fields = new ArrayList<>(); - for (int i = 0; i < payloadFieldsParseNode.jjtGetNumChildren(); i++) { + for (int i = 0; i < compoundDatatTypeFields.jjtGetNumChildren(); i++) { SimpleNode payloadFieldParseNode = assertSimpleNodeType( - payloadFieldsParseNode.jjtGetChild(i), - TapirParserTreeConstants.JJTPAYLOADFIELD + compoundDatatTypeFields.jjtGetChild(i), + TapirParserTreeConstants.JJTDATATYPEFIELD ); int numFieldNodes = payloadFieldParseNode.jjtGetNumChildren(); if (numFieldNodes != 2) { @@ -74,13 +156,13 @@ public class NodeTransformer { String fieldName = getStringValue( assertSimpleNodeType( payloadFieldParseNode.jjtGetChild(0), - TapirParserTreeConstants.JJTPAYLOADFIELDNAME + TapirParserTreeConstants.JJTDATATYPEFIELDNAME ) ); String fieldType = getStringValue( assertSimpleNodeType( payloadFieldParseNode.jjtGetChild(1), - TapirParserTreeConstants.JJTPAYLOADFIELDTYPE + TapirParserTreeConstants.JJTDATATYPEFIELDTYPE ) ); fields.add(new FieldNode(fieldName, fieldType)); @@ -92,7 +174,7 @@ public class NodeTransformer { int numPathSegments = pathsParseNode.jjtGetNumChildren(); ArrayList segments = new ArrayList<>(); for (int i = 0; i < numPathSegments; i++) { - SimpleNode segmentParseNode = assertSimpleNodeType(pathsParseNode.jjtGetChild(i), TapirParserTreeConstants.JJTPATH); + SimpleNode segmentParseNode = assertSimpleNodeType(pathsParseNode.jjtGetChild(i), TapirParserTreeConstants.JJTPATHSEGMENT); segments.add(getStringValue(segmentParseNode)); } return new PathsNode(segments); diff --git a/parser/src/main/java/nu/zoom/tapir/TargetGenerator.java b/parser/src/main/java/nu/zoom/tapir/TargetGenerator.java index 7c95f5a..c47d4e2 100644 --- a/parser/src/main/java/nu/zoom/tapir/TargetGenerator.java +++ b/parser/src/main/java/nu/zoom/tapir/TargetGenerator.java @@ -16,24 +16,23 @@ 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; + private final List dataTypes; + private final boolean verbose ; public static class TargetGeneratorException extends Exception { - public TargetGeneratorException(String message) { - super(message); - } public TargetGeneratorException(Exception cause) { super(cause); } } public TargetGenerator( - final boolean verbose, + boolean verbose, Path outputPath, Path templatePath, - List endpoints + List endpoints, + List dataTypes ) { this.verbose = verbose; this.outputPath = Objects.requireNonNull( @@ -44,26 +43,48 @@ public class TargetGenerator { templatePath, "Template path is required" ); - this.endpoints = Objects.requireNonNull(endpoints) ; + this.endpoints = Objects.requireNonNull(endpoints); + this.dataTypes = Objects.requireNonNull(dataTypes); } - 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); + public void generate() throws TargetGeneratorException { + try { + 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); + List templates = Files + .list(this.templatePath) + .filter(Files::isRegularFile) + .filter(f -> f.getFileName().toString().endsWith(".ftl")) + .toList() ; + for (Path templatePath : templates) { + try (var outputFile = Files.newBufferedWriter( + outputName(templatePath), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING + )) { + if (this.verbose) { + System.out.println("Processing " + templatePath); + } + HashMap templateData = new HashMap<>(); + templateData.put("endpoints", endpoints); + templateData.put("datatypes", dataTypes); + cfg.getTemplate( + templatePath.getFileName().toString() + ).process(templateData, outputFile); + } + } + } catch (TemplateException | IOException ex) { + throw new TargetGeneratorException(ex); } } + + private Path outputName(Path templatePath) { + String name = templatePath.getFileName().toString().replace(".ftl", ".scala") ; + return this.outputPath.resolve(name) ; + } } diff --git a/parser/src/main/jjtree/tapir.jjt b/parser/src/main/jjtree/tapir.jjt index 72355da..4f38e04 100755 --- a/parser/src/main/jjtree/tapir.jjt +++ b/parser/src/main/jjtree/tapir.jjt @@ -23,6 +23,7 @@ SKIP: { TOKEN : { | + | | "> | | @@ -32,63 +33,75 @@ TOKEN : { | ()* > } +void pathSegment() : +{Token t;} +{ + t={jjtThis.value = t.image;} +} + void path() : -{Token t;} -{ - t={jjtThis.value = t.image;} -} - -void paths() : {} { - path() (path())* + pathSegment() (pathSegment())* } -void payloadFieldName() : +void dataTypeFieldType() : {Token t;} { t={jjtThis.value = t.image;} } -void payloadFieldType() : +void dataTypeFieldName() : {Token t;} { t={jjtThis.value = t.image;} } -void payloadField() : +void dataTypeField() : {} { - payloadFieldName() payloadFieldType() + dataTypeFieldName() dataTypeFieldType() } -void payloadFields() : +void dataTypeFields() : {} { - payloadField() ( payloadField() )* + dataTypeField() ( dataTypeField() )* } -void handlerName() : +void compundDataTypeName() : {Token t;} { t={jjtThis.value = t.image;} } -void handlerSpec() : +void compoundDataType() : {} { - handlerName() payloadFields() + compundDataTypeName() dataTypeFields() +} + +void dataTypes() : +{} +{ + (compoundDataType() )* } void endpoint() : {} { - paths() handlerSpec() + path() dataTypeFields() } -SimpleNode endpoints() : +void endpoints() : {} { (endpoint() )* +} + +SimpleNode specification() : +{} +{ + dataTypes() endpoints() { return jjtThis; } } diff --git a/tapir-templates/codec.ftl b/tapir-templates/codec.ftl new file mode 100644 index 0000000..ec48ffe --- /dev/null +++ b/tapir-templates/codec.ftl @@ -0,0 +1,8 @@ +object Codecs: + +<#list datatypes as datatype> + given Codec[${datatype.name?cap_first}] = deriveCodec + +<#list endpoints as endpoint> + given Codec[${endpoint.handler.name?cap_first}Payload] = deriveCodec + \ No newline at end of file diff --git a/tapir-templates/endpoints.ftl b/tapir-templates/endpoints.ftl index d0c8157..7a01e51 100644 --- a/tapir-templates/endpoints.ftl +++ b/tapir-templates/endpoints.ftl @@ -1,4 +1,4 @@ -package se.senashdev.projekt.api +package se.senashdev.project.api import se.rutdev.projekt.api.HttpProtocol.VersionedResponse import se.rutdev.framework.json.circe.RutUtilsCodec @@ -10,8 +10,16 @@ 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 datatypes as datatype> + case class ${datatype.name}( + <#list datatype.fields as field> + ${field.name} : ${field.type}, + + ) + + <#list endpoints as endpoint> - case class ${endpoint.handler.name?cap_first}( + case class ${endpoint.handler.name?cap_first}Payload( <#list endpoint.handler.fields as field> ${field.name} : ${field.type}, @@ -19,19 +27,15 @@ class Endpoints(override val config: OAuthUtils.OAuthConfig) extends framework.s <#list endpoints as endpoint> - given Codec[${endpoint.handler.name?cap_first}] = deriveCodec + val ${endpoint.handler.name}Endpoint = ApiEndpoint[${endpoint.handler.name?cap_first}Payload, VersionedResponse] = + <#list endpoint.paths.paths> + apiV1Endpoint + .post + <#items as segment> + .in("${segment}") + + .post + .in(jsonBody[${endpoint.handler.name?cap_first}Payload]) + .out(jsonBody[VersionedResponse]) - - <#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]) -