From 5a7c6e64d77419fbdfb33b4a927fc8d7db769139 Mon Sep 17 00:00:00 2001 From: Ivan Garcia Sainz-Aja Date: Thu, 12 Dec 2024 11:26:15 +0100 Subject: [PATCH] [plugins/zdl-to-openapi] adds support for `operationIdsToInclude` and `operationIdsToExclude` supporting ant-style wildcard matching. --- plugins/zdl-to-openapi/README.md | 26 +++++----- .../sdk/plugins/PathsProcessor.java | 24 ++++++++- .../sdk/plugins/ZDLToOpenAPIGenerator.java | 50 +++++++++++++++---- .../sdk/plugins/ZDLToOpenAPIPlugin.java | 2 +- .../sdk/plugins/PathsProcessorTest.java | 31 ++++++++++++ .../plugins/ZDLToOpenAPIGeneratorTest.java | 22 ++++++++ .../src/test/resources/inline-parameters.zdl | 37 ++++++++++++++ .../zenwave360/sdk/utils/AntStyleMatcher.java | 15 ++++++ .../io/zenwave360/sdk/zdl/ZDLHttpUtils.java | 13 +++-- 9 files changed, 191 insertions(+), 29 deletions(-) create mode 100644 plugins/zdl-to-openapi/src/test/java/io/zenwave360/sdk/plugins/PathsProcessorTest.java create mode 100644 plugins/zdl-to-openapi/src/test/resources/inline-parameters.zdl create mode 100644 zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/utils/AntStyleMatcher.java diff --git a/plugins/zdl-to-openapi/README.md b/plugins/zdl-to-openapi/README.md index 1a592010..6b660931 100644 --- a/plugins/zdl-to-openapi/README.md +++ b/plugins/zdl-to-openapi/README.md @@ -16,18 +16,20 @@ jbang zw -p io.zenwave360.sdk.plugins.ZDLToOpenAPIPlugin \ ## Options -| **Option** | **Description** | **Type** | **Default** | **Values** | -|--------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------------------------------------|------------| -| `specFile` | Spec file to parse | String | | | -| `targetFolder` | Target folder to generate code to. If left empty, it will print to stdout. | File | | | -| `targetFile` | Target file | String | openapi.yml | | -| `title` | API Title | String | | | -| `idType` | JsonSchema type for id fields and parameters. | String | string | | -| `idTypeFormat` | JsonSchema type format for id fields and parameters. | String | | | -| `zdlBusinessEntityProperty` | Extension property referencing original zdl entity in components schemas (default: x-business-entity) | String | x-business-entity | | -| `zdlBusinessEntityPaginatedProperty` | Extension property referencing original zdl entity in components schemas for paginated lists | String | x-business-entity-paginated | | -| `paginatedDtoItemsJsonPath` | JSONPath list to search for response DTO schemas for list or paginated results. Examples: '$.items' for lists or '$.properties..items' for paginated results. | List | [$.items, $.properties.content.items] | | -| `continueOnZdlError` | Continue even when ZDL contains fatal errors | boolean | true | | +| **Option** | **Description** | **Type** | **Default** | **Values** | +|-------------------------|------------------------------------------------------------------------------------------------------------------------------------|----------|-------------|------------| +| `zdlFile` | ZDL file to parse | String | | | +| `zdlFiles` | ZDL files to parse | List | [] | | +| `title` | API Title | String | | | +| `targetFolder` | Target folder to generate code to. If left empty, it will print to stdout. | File | | | +| `targetFile` | Target file | String | openapi.yml | | +| `idType` | JsonSchema type for id fields and parameters. | String | string | | +| `idTypeFormat` | JsonSchema type format for id fields and parameters. | String | | | +| `dtoPatchSuffix` | DTO Suffix used for schemas in PATCH operations | String | Patch | | +| `operationIdsToInclude` | Operation IDs to include. If empty, all operations will be included. (Supports Ant-style wildcards) | List | | | +| `operationIdsToExclude` | Operation IDs to exclude. If not empty it will be applied to the processed operationIds to include. (Supports Ant-style wildcards) | List | | | +| `continueOnZdlError` | Continue even when ZDL contains fatal errors | boolean | true | | + ## Getting Help diff --git a/plugins/zdl-to-openapi/src/main/java/io/zenwave360/sdk/plugins/PathsProcessor.java b/plugins/zdl-to-openapi/src/main/java/io/zenwave360/sdk/plugins/PathsProcessor.java index d472b71a..e9023aed 100644 --- a/plugins/zdl-to-openapi/src/main/java/io/zenwave360/sdk/plugins/PathsProcessor.java +++ b/plugins/zdl-to-openapi/src/main/java/io/zenwave360/sdk/plugins/PathsProcessor.java @@ -3,6 +3,7 @@ import io.zenwave360.sdk.doc.DocumentedOption; import io.zenwave360.sdk.processors.AbstractBaseProcessor; import io.zenwave360.sdk.processors.Processor; +import io.zenwave360.sdk.utils.AntStyleMatcher; import io.zenwave360.sdk.utils.FluentMap; import io.zenwave360.sdk.utils.JSONPath; import io.zenwave360.sdk.zdl.ZDLFindUtils; @@ -20,6 +21,11 @@ public class PathsProcessor extends AbstractBaseProcessor implements Processor { @DocumentedOption(description = "JsonSchema type format for id fields and parameters.") public String idTypeFormat = null; + public List operationIdsToInclude; + + public List operationIdsToExclude; + + { targetProperty = "zdl"; } @@ -49,6 +55,10 @@ public Map process(Map contextModel) { } } + var operationId = (String) httpOption.getOrDefault("operationId", methodName); + if(!isIncludeOperation(operationId)) { + return; + } var methodVerb = httpOption.get("httpMethod"); var methodPath = ZDLHttpUtils.getPathFromMethod(method); var path = basePath + methodPath; @@ -58,7 +68,7 @@ public Map process(Map contextModel) { var queryParamsMap = ZDLHttpUtils.getQueryParamsAsObject(method, zdl); var hasParams = !pathParams.isEmpty() || !queryParamsMap.isEmpty() || paginated != null; paths.appendTo(path, (String) methodVerb, new FluentMap() - .with("operationId", methodName) + .with("operationId", operationId) .with("httpMethod", methodVerb) .with("tags", new String[]{(String) ((Map)service).get("name")}) .with("summary", method.get("javadoc")) @@ -82,4 +92,16 @@ public Map process(Map contextModel) { return contextModel; } + + protected boolean isIncludeOperation(String operationId) { + if (operationIdsToInclude != null && !operationIdsToInclude.isEmpty()) { + if (operationIdsToInclude.stream().noneMatch(include -> AntStyleMatcher.match(include, operationId))) { + return false; + } + } + if (operationIdsToExclude != null && !operationIdsToExclude.isEmpty()) { + return operationIdsToExclude.stream().noneMatch(exclude -> AntStyleMatcher.match(exclude, operationId)); + } + return true; + } } diff --git a/plugins/zdl-to-openapi/src/main/java/io/zenwave360/sdk/plugins/ZDLToOpenAPIGenerator.java b/plugins/zdl-to-openapi/src/main/java/io/zenwave360/sdk/plugins/ZDLToOpenAPIGenerator.java index 753d716a..227ef2ce 100644 --- a/plugins/zdl-to-openapi/src/main/java/io/zenwave360/sdk/plugins/ZDLToOpenAPIGenerator.java +++ b/plugins/zdl-to-openapi/src/main/java/io/zenwave360/sdk/plugins/ZDLToOpenAPIGenerator.java @@ -11,6 +11,7 @@ import io.zenwave360.sdk.generators.AbstractZDLGenerator; import io.zenwave360.sdk.generators.EntitiesToSchemasConverter; import io.zenwave360.sdk.generators.Generator; +import io.zenwave360.sdk.utils.AntStyleMatcher; import io.zenwave360.sdk.zdl.ZDLFindUtils; import io.zenwave360.sdk.templating.HandlebarsEngine; import io.zenwave360.sdk.templating.OutputFormatType; @@ -36,14 +37,6 @@ public class ZDLToOpenAPIGenerator implements Generator { @DocumentedOption(description = "Target file") public String targetFile = "openapi.yml"; - @DocumentedOption(description = "Extension property referencing original zdl entity in components schemas (default: x-business-entity)") - public String zdlBusinessEntityProperty = "x-business-entity"; - - @DocumentedOption(description = "Extension property referencing original zdl entity in components schemas for paginated lists") - public String zdlBusinessEntityPaginatedProperty = "x-business-entity-paginated"; - - @DocumentedOption(description = "JSONPath list to search for response DTO schemas for list or paginated results. Examples: '$.items' for lists or '$.properties..items' for paginated results.") - public List paginatedDtoItemsJsonPath = List.of("$.items", "$.properties.content.items"); @DocumentedOption(description = "JsonSchema type for id fields and parameters.") public String idType = "string"; @@ -51,6 +44,21 @@ public class ZDLToOpenAPIGenerator implements Generator { @DocumentedOption(description = "JsonSchema type format for id fields and parameters.") public String idTypeFormat = null; + @DocumentedOption(description = "Operation IDs to include. If empty, all operations will be included. (Supports Ant-style wildcards)") + public List operationIdsToInclude; + + @DocumentedOption(description = "Operation IDs to exclude. If not empty it will be applied to the processed operationIds to include. (Supports Ant-style wildcards)") + public List operationIdsToExclude; + + // @DocumentedOption(description = "Extension property referencing original zdl entity in components schemas (default: x-business-entity)") + // public String zdlBusinessEntityProperty = "x-business-entity"; + // + // @DocumentedOption(description = "Extension property referencing original zdl entity in components schemas for paginated lists") + // public String zdlBusinessEntityPaginatedProperty = "x-business-entity-paginated"; + + // @DocumentedOption(description = "JSONPath list to search for response DTO schemas for list or paginated results. Examples: '$.items' for lists or '$.properties..items' for paginated results.") + // public List paginatedDtoItemsJsonPath = List.of("$.items", "$.properties.content.items"); + protected Map httpStatusCodes = Map.of( "get", 200, "post", 201, @@ -117,10 +125,14 @@ public List generate(Map contextModel) { Map schemas = new LinkedHashMap<>(); JSONPath.set(oasSchemas, "components.schemas", schemas); - EntitiesToSchemasConverter converter = new EntitiesToSchemasConverter().withIdType(idType, idTypeFormat).withZdlBusinessEntityProperty(zdlBusinessEntityProperty); + var paths = JSONPath.get(zdlModel, "$.services[*].paths", List.of()); + + EntitiesToSchemasConverter converter = new EntitiesToSchemasConverter().withIdType(idType, idTypeFormat); var methodsWithRest = JSONPath.get(zdlModel, "$.services[*].methods[*][?(@.options.get || @.options.post || @.options.put || @.options.delete || @.options.patch)]", Collections.emptyList()); + methodsWithRest = filterOperationsToInclude(methodsWithRest); List> entities = filterSchemasToInclude(zdlModel, methodsWithRest); + for (Map entity : entities) { String entityName = (String) entity.get("name"); Map openAPISchema = converter.convertToSchema(entity, zdlModel); @@ -135,7 +147,6 @@ public List generate(Map contextModel) { Map paginatedSchema = new HashMap<>(); paginatedSchema.put("allOf", List.of( Map.of("$ref", "#/components/schemas/Page"), - Map.of(zdlBusinessEntityPaginatedProperty, entityName), Map.of("properties", Map.of("content", Maps.of("type", "array", "items", Map.of("$ref", "#/components/schemas/" + entityName)))))); @@ -144,7 +155,9 @@ public List generate(Map contextModel) { } var methodsWithPatch = JSONPath.get(zdlModel, "$.services[*].methods[*][?(@.options.patch)]", Collections.emptyList()); + methodsWithPatch = filterOperationsToInclude(methodsWithPatch); List entitiesForPatch = methodsWithPatch.stream().map(method -> (String) method.get("parameter")).collect(Collectors.toList()); + for (String entityName : entitiesForPatch) { if (entityName != null) { schemas.put(entityName + dtoPatchSuffix, Map.of("allOf", List.of(Map.of("$ref", "#/components/schemas/" + entityName)))); @@ -163,6 +176,23 @@ public List generate(Map contextModel) { return List.of(generateTemplateOutput(contextModel, zdlToOpenAPITemplate, zdlModel, openAPISchemasString)); } + protected List filterOperationsToInclude(List methods) { + List includedMethods = methods; + if (operationIdsToInclude != null && !operationIdsToInclude.isEmpty()) { + includedMethods = methods.stream() + .filter(method -> operationIdsToInclude.stream() + .anyMatch(include -> AntStyleMatcher.match(include, (String) method.get("name")))) + .toList(); + } + if (operationIdsToExclude != null && !operationIdsToExclude.isEmpty()) { + includedMethods = includedMethods.stream() + .filter(method -> operationIdsToExclude.stream() + .noneMatch(exclude -> AntStyleMatcher.match(exclude, (String) method.get("name")))) + .toList(); + } + return includedMethods; + } + protected List> filterSchemasToInclude(Map model, List methodsWithCommands) { Map allEntitiesAndEnums = (Map) model.get("allEntitiesAndEnums"); Map relationships = (Map) model.get("relationships"); diff --git a/plugins/zdl-to-openapi/src/main/java/io/zenwave360/sdk/plugins/ZDLToOpenAPIPlugin.java b/plugins/zdl-to-openapi/src/main/java/io/zenwave360/sdk/plugins/ZDLToOpenAPIPlugin.java index d6d32c15..1aa01628 100644 --- a/plugins/zdl-to-openapi/src/main/java/io/zenwave360/sdk/plugins/ZDLToOpenAPIPlugin.java +++ b/plugins/zdl-to-openapi/src/main/java/io/zenwave360/sdk/plugins/ZDLToOpenAPIPlugin.java @@ -7,7 +7,7 @@ import io.zenwave360.sdk.writers.TemplateFileWriter; import io.zenwave360.sdk.writers.TemplateStdoutWriter; -@DocumentedPlugin(value = "Generates a draft OpenAPI definitions from your ZDL entities and services.", shortCode = "zdl-to-openapi") +@DocumentedPlugin(value = "Generates a draft OpenAPI definitions from your ZDL entities and services.", shortCode = "zdl-to-openapi", hiddenOptions = {"apiFile, apiFiles"}) public class ZDLToOpenAPIPlugin extends Plugin { public ZDLToOpenAPIPlugin() { diff --git a/plugins/zdl-to-openapi/src/test/java/io/zenwave360/sdk/plugins/PathsProcessorTest.java b/plugins/zdl-to-openapi/src/test/java/io/zenwave360/sdk/plugins/PathsProcessorTest.java new file mode 100644 index 00000000..4165b90c --- /dev/null +++ b/plugins/zdl-to-openapi/src/test/java/io/zenwave360/sdk/plugins/PathsProcessorTest.java @@ -0,0 +1,31 @@ +package io.zenwave360.sdk.plugins; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import io.zenwave360.sdk.parsers.ZDLParser; +import io.zenwave360.sdk.processors.ZDLProcessor; +import io.zenwave360.sdk.templating.TemplateOutput; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +public class PathsProcessorTest { + + + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + + private Map loadZDLModelFromResource(String resource) throws Exception { + Map model = new ZDLParser().withZdlFile(resource).parse(); + model = new ZDLProcessor().process(model); + return model; + } + + @Test + public void test_process_inline_parameters() throws Exception { + Map model = loadZDLModelFromResource("classpath:inline-parameters.zdl"); + model = new PathsProcessor().process(model); + List> paths = (List>) model.get("paths"); + } +} diff --git a/plugins/zdl-to-openapi/src/test/java/io/zenwave360/sdk/plugins/ZDLToOpenAPIGeneratorTest.java b/plugins/zdl-to-openapi/src/test/java/io/zenwave360/sdk/plugins/ZDLToOpenAPIGeneratorTest.java index e744cbbf..79c13fa4 100644 --- a/plugins/zdl-to-openapi/src/test/java/io/zenwave360/sdk/plugins/ZDLToOpenAPIGeneratorTest.java +++ b/plugins/zdl-to-openapi/src/test/java/io/zenwave360/sdk/plugins/ZDLToOpenAPIGeneratorTest.java @@ -35,6 +35,28 @@ public void test_customer_address_zdl_to_openapi() throws Exception { System.out.println(outputTemplates.get(0).getContent()); } + @Test + public void test_operationIdsToIncludeExclude() throws Exception { + Map model = loadZDLModelFromResource("classpath:io/zenwave360/sdk/resources/zdl/customer-address.zdl"); + ZDLToOpenAPIGenerator generator = new ZDLToOpenAPIGenerator(); + generator.operationIdsToInclude = List.of("getCustomer", "listCustomers"); + generator.operationIdsToExclude = List.of("getCustomer"); + var processor = new PathsProcessor(); + processor.operationIdsToInclude = generator.operationIdsToInclude; + processor.operationIdsToExclude = generator.operationIdsToExclude; + model = processor.process(model); + + List outputTemplates = generator.generate(model); + Assertions.assertEquals(1, outputTemplates.size()); + var apiText = outputTemplates.get(0).getContent(); + + System.out.println(apiText); + + Assertions.assertTrue(apiText.contains("listCustomers")); + Assertions.assertFalse(apiText.contains("getCustomer")); + Assertions.assertFalse(apiText.contains("updateCustomer")); + } + @Test public void test_order_faults_zdl_to_openapi() throws Exception { Map model = loadZDLModelFromResource("classpath:io/zenwave360/sdk/resources/zdl/order-faults-attachments-model.zdl"); diff --git a/plugins/zdl-to-openapi/src/test/resources/inline-parameters.zdl b/plugins/zdl-to-openapi/src/test/resources/inline-parameters.zdl new file mode 100644 index 00000000..3e40221c --- /dev/null +++ b/plugins/zdl-to-openapi/src/test/resources/inline-parameters.zdl @@ -0,0 +1,37 @@ +@aggregate +entity MetricRecord { + +} + + +@inline +input MetricsSummarySearchCriteria { + hisNumber String required maxlength(100) +} + +@inline +input MetricsSearchCriteria { + hisNumber String required maxlength(100) + dateFrom LocalDateTime + dateTo LocalDateTime +} + +output Metric { + +} + +@rest("/metrics") +service MedicalRecordService for (MetricRecord) { + // esto es para la webapp + @get("/{hisNumber}/daily") @paginated + getDailyMetrics(MetricsSearchCriteria) Metric[] + // esto es para la webapp + @get("/{hisNumber}/frequently") @paginated + getFrequestMetrics(MetricsSearchCriteria) Metric[] + + /** + * Summary metrics for mobile + */ + @get("/{hisNumber}/summary") + getMetricsSummary(MetricsSummarySearchCriteria) Metric[] +} diff --git a/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/utils/AntStyleMatcher.java b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/utils/AntStyleMatcher.java new file mode 100644 index 00000000..03cd7a88 --- /dev/null +++ b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/utils/AntStyleMatcher.java @@ -0,0 +1,15 @@ +package io.zenwave360.sdk.utils; + +import java.util.regex.Pattern; + +public class AntStyleMatcher { + + public static boolean match(String pattern, String filePath) { + // Convert Ant-style pattern to regex + String regex = pattern + .replace("**", ".*") + .replace("*", "[^/]*") + .replace("?", "."); + return Pattern.matches(regex, filePath); + } +} diff --git a/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/zdl/ZDLHttpUtils.java b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/zdl/ZDLHttpUtils.java index c69a07dd..5e7e2291 100644 --- a/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/zdl/ZDLHttpUtils.java +++ b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/zdl/ZDLHttpUtils.java @@ -56,11 +56,14 @@ public static List> getQueryParamsAsObject(Map method, Map z } } } - return (List) params.entrySet().stream().filter(entry -> !pathParams.contains(entry.getKey())).map(entry -> { - var type = entry.getValue(); - var typeAndFormat = EntitiesToSchemasConverter.schemaTypeAndFormat((String) type); - return Maps.of("name", entry.getKey(),"type", typeAndFormat.get("type"), "format", typeAndFormat.get("format")); - }).toList(); + return (List) params.entrySet().stream() + .filter(entry -> !pathParams.contains(entry.getKey())) + .map(entry -> { + var type = entry.getValue(); + var typeAndFormat = EntitiesToSchemasConverter.schemaTypeAndFormat((String) type); + return Maps.of("name", entry.getKey(), "type", typeAndFormat.get("type"), "format", typeAndFormat.get("format")); + }) + .toList(); } public static List getPathParamsFromMethod(Map method) { var path = getPathFromMethod(method);