Skip to content

Commit

Permalink
[plugins/zdl-to-openapi] adds support for operationIdsToInclude and…
Browse files Browse the repository at this point in the history
… `operationIdsToExclude` supporting ant-style wildcard matching.
  • Loading branch information
ivangsa committed Dec 12, 2024
1 parent 141becc commit 5a7c6e6
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 29 deletions.
26 changes: 14 additions & 12 deletions plugins/zdl-to-openapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<content property>.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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String> operationIdsToInclude;

public List<String> operationIdsToExclude;


{
targetProperty = "zdl";
}
Expand Down Expand Up @@ -49,6 +55,10 @@ public Map<String, Object> process(Map<String, Object> 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;
Expand All @@ -58,7 +68,7 @@ public Map<String, Object> process(Map<String, Object> 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"))
Expand All @@ -82,4 +92,16 @@ public Map<String, Object> process(Map<String, Object> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,21 +37,28 @@ 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.<content property>.items' for paginated results.")
public List<String> paginatedDtoItemsJsonPath = List.of("$.items", "$.properties.content.items");

@DocumentedOption(description = "JsonSchema type for id fields and parameters.")
public String idType = "string";

@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<String> 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<String> 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.<content property>.items' for paginated results.")
// public List<String> paginatedDtoItemsJsonPath = List.of("$.items", "$.properties.content.items");

protected Map<String, Integer> httpStatusCodes = Map.of(
"get", 200,
"post", 201,
Expand Down Expand Up @@ -117,10 +125,14 @@ public List<TemplateOutput> generate(Map<String, Object> contextModel) {
Map<String, Object> 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.<Map>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.<Map>emptyList());
methodsWithRest = filterOperationsToInclude(methodsWithRest);
List<Map<String, Object>> entities = filterSchemasToInclude(zdlModel, methodsWithRest);

for (Map<String, Object> entity : entities) {
String entityName = (String) entity.get("name");
Map<String, Object> openAPISchema = converter.convertToSchema(entity, zdlModel);
Expand All @@ -135,7 +147,6 @@ public List<TemplateOutput> generate(Map<String, Object> contextModel) {
Map<String, Object> 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))))));
Expand All @@ -144,7 +155,9 @@ public List<TemplateOutput> generate(Map<String, Object> contextModel) {
}

var methodsWithPatch = JSONPath.get(zdlModel, "$.services[*].methods[*][?(@.options.patch)]", Collections.<Map>emptyList());
methodsWithPatch = filterOperationsToInclude(methodsWithPatch);
List<String> 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))));
Expand All @@ -163,6 +176,23 @@ public List<TemplateOutput> generate(Map<String, Object> contextModel) {
return List.of(generateTemplateOutput(contextModel, zdlToOpenAPITemplate, zdlModel, openAPISchemasString));
}

protected List<Map> filterOperationsToInclude(List<Map> methods) {
List<Map> 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<Map<String, Object>> filterSchemasToInclude(Map<String, Object> model, List<Map> methodsWithCommands) {
Map<String, Object> allEntitiesAndEnums = (Map) model.get("allEntitiesAndEnums");
Map<String, Object> relationships = (Map) model.get("relationships");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Object> loadZDLModelFromResource(String resource) throws Exception {
Map<String, Object> model = new ZDLParser().withZdlFile(resource).parse();
model = new ZDLProcessor().process(model);
return model;
}

@Test
public void test_process_inline_parameters() throws Exception {
Map<String, Object> model = loadZDLModelFromResource("classpath:inline-parameters.zdl");
model = new PathsProcessor().process(model);
List<Map<String, Object>> paths = (List<Map<String, Object>>) model.get("paths");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object> 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<TemplateOutput> 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<String, Object> model = loadZDLModelFromResource("classpath:io/zenwave360/sdk/resources/zdl/order-faults-attachments-model.zdl");
Expand Down
37 changes: 37 additions & 0 deletions plugins/zdl-to-openapi/src/test/resources/inline-parameters.zdl
Original file line number Diff line number Diff line change
@@ -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[]
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 5a7c6e6

Please sign in to comment.