Skip to content

Commit

Permalink
[plugins/openapi-karate] adds OpenAPIKarateGenerator
Browse files Browse the repository at this point in the history
  • Loading branch information
ivangsa committed Aug 4, 2024
1 parent a172b25 commit 9b7d7d3
Show file tree
Hide file tree
Showing 19 changed files with 507 additions and 4 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ jobs:
./plugins/zdl-to-asyncapi/target/site/jacoco/jacoco.csv
./plugins/zdl-to-markdown/target/site/jacoco/jacoco.csv
./plugins/openapi-spring-webtestclient/target/site/jacoco/jacoco.csv
./plugins/openapi-karate/target/site/jacoco/jacoco.csv
./zenwave-sdk-cli/target/site/jacoco/jacoco.csv
- name: Log coverage percentage
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/publish-maven-central.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jobs:
./plugins/zdl-to-asyncapi/target/site/jacoco/jacoco.csv
./plugins/zdl-to-markdown/target/site/jacoco/jacoco.csv
./plugins/openapi-spring-webtestclient/target/site/jacoco/jacoco.csv
./plugins/openapi-karate/target/site/jacoco/jacoco.csv
./zenwave-sdk-cli/target/site/jacoco/jacoco.csv
- name: Log coverage percentage
Expand Down
2 changes: 2 additions & 0 deletions jbang-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"io.github.zenwave360.zenwave-sdk.plugins:asyncapi-spring-cloud-streams3:RELEASE",
"io.github.zenwave360.zenwave-sdk.plugins:asyncapi-jsonschema2pojo:RELEASE",
"io.github.zenwave360.zenwave-sdk.plugins:openapi-spring-webtestclient:RELEASE",
"io.github.zenwave360.zenwave-sdk.plugins:openapi-karate:RELEASE",
"io.github.zenwave360.zenwave-sdk.plugins:backend-application-default:RELEASE",
"io.github.zenwave360.zenwave-sdk.plugins:zdl-to-openapi:RELEASE",
"io.github.zenwave360.zenwave-sdk.plugins:zdl-to-asyncapi:RELEASE",
Expand All @@ -24,6 +25,7 @@
"io.github.zenwave360.zenwave-sdk.plugins:asyncapi-spring-cloud-streams3:1.7.0-SNAPSHOT",
"io.github.zenwave360.zenwave-sdk.plugins:asyncapi-jsonschema2pojo:1.7.0-SNAPSHOT",
"io.github.zenwave360.zenwave-sdk.plugins:openapi-spring-webtestclient:1.7.0-SNAPSHOT",
"io.github.zenwave360.zenwave-sdk.plugins:openapi-karate:1.7.0-SNAPSHOT",
"io.github.zenwave360.zenwave-sdk.plugins:backend-application-default:1.7.0-SNAPSHOT",
"io.github.zenwave360.zenwave-sdk.plugins:zdl-to-openapi:1.7.0-SNAPSHOT",
"io.github.zenwave360.zenwave-sdk.plugins:zdl-to-asyncapi:1.7.0-SNAPSHOT",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ protected boolean is(Map<String, Object> model, String... annotations) {
return !(JSONPath.get(model, "$.entity.options[?(" + annotationsFilter + ")]", List.of())).isEmpty();
}

protected Function<Map<String, Object>, Boolean> skipEntityRepository = (model) -> !(is(model, "aggregate") || ZDLFindUtils.isAggregateRoot(JSONPath.get(model, "zdl"), JSONPath.get(model, "$.entity.name")));
protected Function<Map<String, Object>, Boolean> skipEntityRepository = (model) -> is(model, "persistence") // if polyglot persistence -> skip
|| !(is(model, "aggregate") || ZDLFindUtils.isAggregateRoot(JSONPath.get(model, "zdl"), JSONPath.get(model, "$.entity.name")));
protected Function<Map<String, Object>, Boolean> skipEntityId = (model) -> is(model, "embedded", "vo", "input", "abstract");
protected Function<Map<String, Object>, Boolean> skipEntity = (model) -> is(model, "vo", "input");
protected Function<Map<String, Object>, Boolean> skipEntityInput = (model) -> inputDTOSuffix == null || inputDTOSuffix.isEmpty();
Expand Down
40 changes: 40 additions & 0 deletions plugins/openapi-karate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Spring WebTestClient Generator
> 👉 ZenWave360 Helps You Create Software Easy to Understand
Generates test for KarateDSL based on OpenAPI and Arazzo specifications.

```shell
jbang zw -p io.zenwave360.sdk.plugins.OpenAPIKaratePlugin \
specFile=src/main/resources/model/openapi.yml \
targetFolder=src/test/java \
testsPackage=io.zenwave360.example.adapters.web.tests \
groupBy=service
```

```shell
jbang zw -p io.zenwave360.sdk.plugins.OpenAPIKaratePlugin \
specFile=src/main/resources/model/openapi.yml \
targetFolder=src/test/java \
testsPackage=io.zenwave360.example.adapters.web.tests \
groupBy=businessFlow \
businessFlowTestName=CustomerCRUDTest \
operationIds=createCustomer,getCustomer,updateCustomer,deleteCustomer
```

## Options

| **Option** | **Description** | **Type** | **Default** | **Values** |
|--------------------------------|------------------------------------------------------------------------------|-------------|----------------------------------------------------------|-------------------------------------------|
| `specFile` | API Specification File | URI | | |
| `targetFolder` | Target folder to generate code to. If left empty, it will print to stdout. | File | | |
| `basePackage` | Applications base package | String | | |
| `testsPackage` | Package name for generated tests | String | {{basePackage}}.adapters.web.tests | |
| `groupBy` | Generate test classes grouped by | GroupByType | service | service, operation, partial, businessFlow |
| `operationIds` | OpenAPI operationIds to generate code for | List | [] | |
| `businessFlowTestName` | Business Flow Test name | String | | |

## Getting Help

```shell
jbang zw -p io.zenwave360.sdk.plugins.OpenAPIKaratePlugin --help
```
14 changes: 14 additions & 0 deletions plugins/openapi-karate/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?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>io.github.zenwave360.zenwave-sdk</groupId>
<artifactId>plugins-parent</artifactId>
<version>1.7.0-SNAPSHOT</version>
</parent>
<name>${project.groupId}:${project.artifactId}</name>
<groupId>io.github.zenwave360.zenwave-sdk.plugins</groupId>
<artifactId>openapi-karate</artifactId>
<packaging>jar</packaging>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package io.zenwave360.sdk.plugins;

import java.util.*;
import java.util.stream.Collectors;

import io.zenwave360.sdk.generators.JsonSchemaToJsonFaker;
import io.zenwave360.sdk.options.WebFlavorType;
import io.zenwave360.sdk.utils.JSONPath;
import io.zenwave360.sdk.utils.NamingUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;

import io.zenwave360.sdk.doc.DocumentedOption;
import io.zenwave360.sdk.generators.AbstractOpenAPIGenerator;
import io.zenwave360.sdk.parsers.Model;
import io.zenwave360.sdk.templating.HandlebarsEngine;
import io.zenwave360.sdk.templating.TemplateEngine;
import io.zenwave360.sdk.templating.TemplateInput;
import io.zenwave360.sdk.templating.TemplateOutput;

import static io.zenwave360.sdk.templating.OutputFormatType.GERKIN;
import static io.zenwave360.sdk.templating.OutputFormatType.JAVA;
import static org.apache.commons.lang3.ObjectUtils.firstNonNull;

public class OpenAPIKarateGenerator extends AbstractOpenAPIGenerator {

enum GroupByType {
service, operation, businessFlow
}

public String apiProperty = "api";

@DocumentedOption(description = "Package name for generated Karate tests")
public String testsPackage = "{{basePackage}}.adapters.web";

public boolean simpleDomainPackaging = false;

@DocumentedOption(description = "Generate features grouped by", required = true)
public GroupByType groupBy = GroupByType.service;

@DocumentedOption(description = "Business Flow Feature name")
public String businessFlowTestName;

private HandlebarsEngine handlebarsEngine = new HandlebarsEngine();

private JsonSchemaToJsonFaker jsonSchemaToJsonFaker = new JsonSchemaToJsonFaker();

private final String prefix = "io/zenwave360/sdk/plugins/OpenAPIKarateGenerator/";
private final TemplateInput partialTemplate = new TemplateInput(prefix + "partials/Operation.feature", "src/test/resources/{{asPackageFolder testsPackage}}/Operation.feature");
// private final TemplateInput testSetTemplate = new TemplateInput(prefix + "ControllersTestSet.java", "{{asPackageFolder testsPackage}}/ControllersTestSet.java").withMimeType(JAVA);

private final TemplateInput businessFlowTestTemplate = new TemplateInput(prefix + "BusinessFlowTest.feature", "src/test/resources/{{asPackageFolder testsPackage}}/{{businessFlowTestName}}.feature").withMimeType(GERKIN);
private final TemplateInput serviceTestTemplate = new TemplateInput(prefix + "Service.feature", "src/test/resources/{{asPackageFolder testsPackage}}/{{serviceName}}.feature").withMimeType(GERKIN);
private final TemplateInput operationTestTemplate = new TemplateInput(prefix + "Operation.feature", "src/test/resources/{{asPackageFolder testsPackage}}/{{serviceName}}/{{asJavaTypeName operationId}}.feature").withMimeType(GERKIN);

@Override
public void onPropertiesSet() {
if(basePackage == null) {
basePackage = testsPackage;
}
if (simpleDomainPackaging) {
testsPackage = "{{basePackage}}";
}
}

public TemplateEngine getTemplateEngine() {
return handlebarsEngine;
}

Model getApiModel(Map<String, Object> contextModel) {
return (Model) contextModel.get(apiProperty);
}

@Override
public List<TemplateOutput> generate(Map<String, Object> contextModel) {
List<TemplateOutput> templateOutputList = new ArrayList<>();
Model apiModel = getApiModel(contextModel);
Map<String, List<Map<String, Object>>> operationsByTag = getOperationsGroupedByTag(apiModel);

if (groupBy == GroupByType.businessFlow) {
List<Map<String, Object>> operations = getOperationsByOperationIds(apiModel, operationIds);
templateOutputList.add(generateTemplateOutput(contextModel, businessFlowTestTemplate, null, operations));
}

if (groupBy == GroupByType.service || groupBy == GroupByType.operation) {

List<String> includedTestNames = new ArrayList<>();
List<String> includedImports = new ArrayList<>();

for (Map.Entry<String, List<Map<String, Object>>> entry : operationsByTag.entrySet()) {
String serviceName = apiServiceName(entry.getKey());
if(groupBy == GroupByType.service) {
includedTestNames.add(serviceName);
templateOutputList.add(generateTemplateOutput(contextModel, serviceTestTemplate, serviceName, entry.getValue()));
} else {
List<Map<String, Object>> operations = entry.getValue();
includedTestNames.addAll(operations.stream().map(o -> NamingUtils.asJavaTypeName((String) o.get("operationId"))).collect(Collectors.toList()));
includedImports.add(serviceName);
for (Map<String, Object> operation : operations) {
templateOutputList.add(generateTemplateOutput(contextModel, operationTestTemplate, serviceName, List.of(operation)));
}
}
}

}

return templateOutputList;
}

{
handlebarsEngine.getHandlebars().registerHelper("requestExample", (operation, options) -> {
return jsonSchemaToJsonFaker.generateExampleAsJson((Map) operation);
});
handlebarsEngine.getHandlebars().registerHelper("karatePath", (operation, options) -> {
String path = JSONPath.get(operation, "x--path");
return String.join("", "'", path, "'")
.replace("{", "', pathParams.")
.replace("}", ", '")
.replace(", ''", "");
});

handlebarsEngine.getHandlebars().registerHelper("queryParams", (operation, options)
-> JSONPath.get(operation, "parameters", Collections.<Map>emptyList()).stream().filter(p -> "query" .equals(p.get("in"))).collect(Collectors.toList()));

handlebarsEngine.getHandlebars().registerHelper("pathParams", (operation, options)
-> JSONPath.get(operation, "parameters", Collections.<Map>emptyList()).stream().filter(p -> "path" .equals(p.get("in"))).collect(Collectors.toList()));

handlebarsEngine.getHandlebars().registerHelper("paramsExample", (params, options) -> {
return ((Collection<Map>) params).stream()
.map(p -> p.get("name") + ": " + firstNonNull(p.get("example"), jsonSchemaToJsonFaker.generateExample((Map<String, Object>) p.get("schema"))))
.collect(Collectors.joining(", "));
});

}

private String apiServiceName(String tag) {
return NamingUtils.asJavaTypeName(tag) + "Api";
}

public TemplateOutput generateTemplateOutput(Map<String, Object> contextModel, TemplateInput template, String serviceName, List<Map<String, Object>> operations) {
Map<String, Object> model = new HashMap<>();
model.putAll(this.asConfigurationMap());
model.put("context", contextModel);
model.put("openapi", getApiModel(contextModel));
model.put("serviceName", serviceName);
model.put("operations", operations);
if(operations != null && operations.size() == 1) {
model.put("operationId", operations.get(0).get("operationId"));
model.put("operation", operations.get(0));
}
return getTemplateEngine().processTemplate(model, template).get(0);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.zenwave360.sdk.plugins;

import io.zenwave360.sdk.Plugin;
import io.zenwave360.sdk.doc.DocumentedPlugin;
import io.zenwave360.sdk.parsers.DefaultYamlParser;
import io.zenwave360.sdk.processors.OpenApiProcessor;
import io.zenwave360.sdk.writers.TemplateFileWriter;
import io.zenwave360.sdk.writers.TemplateStdoutWriter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static io.zenwave360.sdk.plugins.OpenAPIKarateGenerator.GroupByType.businessFlow;

@DocumentedPlugin(value = "Generates test for SpringMVC or Spring WebFlux using WebTestClient based on OpenAPI specification.", shortCode = "spring-webtestclient")
public class OpenAPIKaratePlugin extends Plugin {

private Logger log = LoggerFactory.getLogger(getClass());
public OpenAPIKaratePlugin() {
super();
withChain(DefaultYamlParser.class, OpenApiProcessor.class, OpenAPIKarateGenerator.class, /* JavaFormatter.class, */ TemplateFileWriter.class);
}

@Override
public <T extends Plugin> T processOptions() {

if(hasOption("groupBy", businessFlow) && !hasOption("businessFlowTestName")) {
log.info("Business flow test name option 'businessFlowTestName' not provided. Printing to stdout.");
replaceInChain(TemplateFileWriter.class, TemplateStdoutWriter.class);
withOption("businessFlowTestName", "BusinessFlowTest");
}
withOption("DefaultYamlParser.specFile", StringUtils.firstNonBlank((String) getOptions().get("openapiFile"), this.getSpecFile()));
return (T) this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@openapi-file={{openapiFile}}
Feature: {{businessFlowTestName}}

Background:
* url baseUrl
# * def auth = { username: '', password: '' }
* def authHeader = call read('classpath:karate-auth.js') auth
* configure headers = authHeader || {}

@business-flow
@operationId={{operationIds}}
Scenario: {{businessFlowTestName}}

{{#each operations as |operation| ~}}
# {{operation.operationId}}
{{~> (partial 'partials/Operation.feature')}}

{{/each~}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@openapi-file={{openapiFile}}
Feature: {{operationId}}

Background:
* url baseUrl
# * def auth = { username: '', password: '' }
* def authHeader = call read('classpath:karate-auth.js') auth
* configure headers = authHeader || {}

{{#each operations as |operation| ~}}
@operationId={{operationId}}
Scenario: {{operationId}}
{{> (partial 'partials/Operation.feature')}}

{{/each~}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@openapi-file={{openapiFile}}
Feature: {{serviceName}}

Background:
* url baseUrl
# * def auth = { username: '', password: '' }
* def authHeader = call read('classpath:karate-auth.js') auth
* configure headers = authHeader || {}

{{#each operations as |operation| ~}}
@operationId={{operationId}}
Scenario: {{operationId}}
{{> (partial 'partials/Operation.feature')}}

{{/each~}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{{~#if (pathParams operation)}}
* def pathParams = { {{{paramsExample (pathParams operation)}}} }
{{~/if}}
Given path {{{karatePath operation}}}
{{~#if (queryParams operation)}}
And def queryParams = {{{paramsExample (queryParams operation)}}}
{{~/if}}
{{~#if operation.x--request-dto}}
And request
"""
{{{requestExample operation.x--request-schema}}}
"""
{{~/if}}
When method {{operation.x--httpVerb}}
Then status {{operation.x--response.x--statusCode}}
{{~#if operation.x--response}}
* def {{operation.operationId}}Response = response
{{~#if operation.x--response.x--response-schema}}
* def {{asInstanceName operation.x--response.x--response-dto}}Id = response.id
{{~/if}}
{{~/if}}
Loading

0 comments on commit 9b7d7d3

Please sign in to comment.