diff --git a/modules/openapi-generator/pom.xml b/modules/openapi-generator/pom.xml index 112bcc4d980c..1f9db5a5febd 100644 --- a/modules/openapi-generator/pom.xml +++ b/modules/openapi-generator/pom.xml @@ -297,6 +297,11 @@ commons-lang3 ${commons-lang.version} + + org.apache.commons + commons-text + ${commons-text.version} + commons-cli commons-cli diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenOperation.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenOperation.java index 8d8e9cdc13e9..3404c933f3cc 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenOperation.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenOperation.java @@ -69,6 +69,10 @@ private static boolean nonempty(List params) { return params != null && params.size() > 0; } + private static boolean nonempty(Map params) { + return params != null && params.size() > 0; + } + /** * Check if there's at least one body parameter * @@ -186,6 +190,15 @@ public boolean getHasDefaultResponse() { return responses.stream().filter(response -> response.isDefault).findFirst().isPresent(); } + /** + * Check if there's at least one vendor extension + * + * @return true if vendor extensions exists, false otherwise + */ + public boolean getHasVendorExtensions() { + return nonempty(vendorExtensions); + } + /** * Check if act as Restful index method * diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/K6ClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/K6ClientCodegen.java index 002084bc6165..df3739a0154b 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/K6ClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/K6ClientCodegen.java @@ -28,22 +28,27 @@ import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; import java.util.Set; +import java.util.TreeMap; import java.util.TreeSet; import java.util.stream.Collectors; import javax.annotation.Nullable; import com.google.common.collect.ImmutableMap; -import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.commons.text.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.openapitools.codegen.CodegenConfig; import org.openapitools.codegen.CodegenConstants; import org.openapitools.codegen.CodegenModel; +import org.openapitools.codegen.CodegenOperation; import org.openapitools.codegen.CodegenParameter; import org.openapitools.codegen.CodegenProperty; import org.openapitools.codegen.CodegenResponse; @@ -56,10 +61,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.samskivert.mustache.Mustache; import com.samskivert.mustache.Mustache.Lambda; import com.samskivert.mustache.Template; +import io.swagger.v3.core.util.Json; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.PathItem; @@ -73,6 +81,24 @@ public class K6ClientCodegen extends DefaultCodegen implements CodegenConfig { + // K6 vendor extension - operation grouping - group operations and define their + // ordering, to allow for e.g. scenario testing + private static final String X_OPERATION_GROUPING = "x-k6-openapi-operation-grouping"; + + // K6 vendor extension - operation response - for now, allows to hide given + // operation response, so that in case of multiple 2xx responses, generated + // script checks only against e.g. code 200 responses + private static final String X_OPERATION_RESPONSE = "x-k6-openapi-operation-response"; + private static final String X_OPERATION_RESPONSE_HIDE = "hide"; + + // K6 vendor extension - extract data from operation - allows to specify path to + // value in body of response which should be extracted and assigned to variable + // for later use by other operations + private static final String X_OPERATION_DATAEXTRACT = "x-k6-openapi-operation-dataextract"; + private static final String X_OPERATION_DATAEXTRACT_OPERATION_ID = "operationId"; // denotes ID of operation whose response body contains value to be extracted + private static final String X_OPERATION_DATAEXTRACT_VALUE_PATH = "valuePath"; // denotes path to value in body of response which should be extracted + private static final String X_OPERATION_DATAEXTRACT_PARAMETER_NAME = "parameterName"; // denotes name of parameter to which extracted value should be assigned + public K6ClientCodegen() { super(); @@ -86,6 +112,7 @@ static class Parameter { String key; Object value; boolean hasExample; + boolean initialize; public Parameter(String key, Object value) { this.key = key; @@ -98,6 +125,11 @@ public Parameter(String key, Object exampleValue, boolean hasExample) { this.hasExample = hasExample; } + public Parameter(String key, boolean initialize) { + this.key = key; + this.initialize = initialize; + } + @Override public int hashCode() { return key.hashCode(); @@ -110,10 +142,35 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) return false; Parameter p = (Parameter) obj; - return key.equals(p.key) && value.equals(p.value) && hasExample == p.hasExample; + return key.equals(p.key) && value.equals(p.value) && hasExample == p.hasExample + && initialize == p.initialize; } } - + + // Stores information specified in `X_OPERATION_GROUPING` K6 vendor extension + static class OperationGrouping { + String groupName; + int order; + + public OperationGrouping(String groupName, int order) { + this.groupName = groupName; + this.order = order; + } + } + + // Stores information specified in `X_OPERATION_DATAEXTRACT` K6 vendor extension + static class DataExtractSubstituteParameter { + String operationId; + String valuePath; + String paramName; + + public DataExtractSubstituteParameter(String operationId, String valuePath, String paramName) { + this.operationId = operationId; + this.valuePath = valuePath; + this.paramName = paramName; + } + } + static class ParameterValueLambda implements Mustache.Lambda { private static final String NO_EXAMPLE_PARAM_VALUE_PREFIX = "TODO_EDIT_THE_"; @@ -123,7 +180,7 @@ public void execute(Template.Fragment fragment, Writer writer) throws IOExceptio // default used if no example is provided String noExampleParamValue = String.join("", quoteExample( - String.join("", NO_EXAMPLE_PARAM_VALUE_PREFIX, fragment.execute())), + String.join("", NO_EXAMPLE_PARAM_VALUE_PREFIX, fragment.execute())), ";", " // specify value as there is no example value for this parameter in OpenAPI spec"); @@ -138,10 +195,10 @@ public void execute(Template.Fragment fragment, Writer writer) throws IOExceptio @SuppressWarnings("unchecked") Set exampleValues = ((Map) rawValue).values().stream() - .map(x -> quoteExample( - StringEscapeUtils.escapeEcmaScript( - String.valueOf(x.getValue())))) - .collect(Collectors.toCollection(() -> new TreeSet())); + .map(x -> quoteExample( + StringEscapeUtils.escapeEcmaScript( + String.valueOf(x.getValue())))) + .collect(Collectors.toCollection(() -> new TreeSet())); if (!exampleValues.isEmpty()) { @@ -160,11 +217,20 @@ public void execute(Template.Fragment fragment, Writer writer) throws IOExceptio quoteExample( StringEscapeUtils.escapeEcmaScript( String.valueOf( - ((K6ClientCodegen.Parameter) fragment.context()).value))), + ((K6ClientCodegen.Parameter) fragment.context()).value))), ";", " // extracted from 'example' field defined at the parameter level of OpenAPI spec")); } + // param needs to be initialized for subsequent data extraction - see `X_OPERATION_DATAEXTRACT` K6 vendor extension + } else if (fragment.context() instanceof K6ClientCodegen.Parameter + && ((K6ClientCodegen.Parameter) fragment.context()).initialize) { + + writer.write(String.join("", + "null", + ";", + " // parameter initialized for subsequent data extraction")); + } else { writer.write(noExampleParamValue); } @@ -231,25 +297,33 @@ public k6Check(Integer status, String description) { static class HTTPRequest { String method; + boolean isDelete; String path; @Nullable List query; @Nullable HTTPBody body; + boolean hasBodyExample; @Nullable HTTPParameters params; @Nullable List k6Checks; + @Nullable + DataExtractSubstituteParameter dataExtract; public HTTPRequest(String method, String path, @Nullable List query, @Nullable HTTPBody body, - @Nullable HTTPParameters params, @Nullable List k6Checks) { + boolean hasBodyExample, @Nullable HTTPParameters params, @Nullable List k6Checks, + DataExtractSubstituteParameter dataExtract) { // NOTE: https://k6.io/docs/javascript-api/k6-http/del-url-body-params this.method = method.equals("delete") ? "del" : method; + this.isDelete = method.equals("delete") ? true : false; this.path = path; this.query = query; this.body = body; + this.hasBodyExample = hasBodyExample; this.params = params; this.k6Checks = k6Checks; + this.dataExtract = dataExtract; } } @@ -257,11 +331,26 @@ static public class HTTPRequestGroup { String groupName; Set variables; // query and path parameters List requests; + private Map requestsMap; - public HTTPRequestGroup(String groupName, Set variables, List requests) { + public HTTPRequestGroup(String groupName, Set variables, Map requestsMap) { this.groupName = groupName; this.variables = variables; - this.requests = requests; + this.requestsMap = requestsMap; + this.requests = sortRequests(requestsMap); + } + + private void addRequests(Map moreRequests) { + this.requestsMap.putAll(moreRequests); + this.requests = sortRequests(this.requestsMap); + } + + private void addVariables(Set moreVariables) { + this.variables.addAll(moreVariables); + } + + private List sortRequests(Map requestsMap) { + return new ArrayList<>(new TreeMap(requestsMap).values()); } } @@ -398,14 +487,21 @@ public void preprocessOpenAPI(OpenAPI openAPI) { } additionalProperties.put(BASE_URL, baseURL); - List requestGroups = new ArrayList<>(); + // if data is to be extracted from any of the operations' responses, this has to + // be known prior to executing processing of OpenAPI spec further down + Map dataExtractSubstituteParams = getDataExtractSubstituteParameters( + openAPI); + + Map requestGroups = new HashMap<>(); Set extraParameters = new HashSet<>(); Map> pathVariables = new HashMap<>(); for (String path : openAPI.getPaths().keySet()) { - List requests = new ArrayList<>(); + Map requests = new HashMap<>(); Set variables = new HashSet<>(); + String groupName = path; + for (Map.Entry methodOperation : openAPI.getPaths().get(path). readOperationsMap().entrySet()) { List httpParams = new ArrayList<>(); @@ -416,11 +512,33 @@ public void preprocessOpenAPI(OpenAPI openAPI) { final Operation operation = methodOperation.getValue(); final PathItem.HttpMethod method = methodOperation.getKey(); + OptionalInt operationGroupingOrder = OptionalInt.empty(); + + final CodegenOperation cgOperation = super.fromOperation(path, method.name(), operation, null); + + String operationId = operation.getOperationId(); + + boolean hasRequestBodyExample = false; + + // optionally group and order operations - see `X_OPERATION_GROUPING` K6 vendor + // extension + Optional operationGrouping = extractOperationGrouping(cgOperation); + if (operationGrouping.isPresent()) { + groupName = operationGrouping.get().groupName; + operationGroupingOrder = OptionalInt.of(operationGrouping.get().order); + } for (Map.Entry resp : operation.getResponses().entrySet()) { String statusData = resp.getKey().equals("default") ? "200" : resp.getKey(); + + // optionally hide given response - see `X_OPERATION_RESPONSE` K6 vendor + // extension + // i.e. in case of multiple 2xx responses, generated script checks only against + // e.g. code 200 responses + boolean hideOperationResponse = shouldHideOperationResponse(resp.getValue()); + int status = Integer.parseInt(statusData); - if (status >= 200 && status < 300) { + if (!hideOperationResponse && (status >= 200 && status < 300)) { k6Checks.add(new k6Check(status, resp.getValue().getDescription())); } } @@ -436,6 +554,12 @@ public void preprocessOpenAPI(OpenAPI openAPI) { RequestBody requestBody = ModelUtils.getReferencedRequestBody(openAPI, operation.getRequestBody()); + // extract request body example, if present + hasRequestBodyExample = hasRequestBodyExample(requestBody, contentTypeValue); + if (hasRequestBodyExample) { + extractRequestBodyExample(requestBody, contentTypeValue, bodyOrFormParams); + } + for (Map.Entry responseEntry : operation.getResponses().entrySet()) { CodegenResponse r = fromResponse(responseEntry.getKey(), responseEntry.getValue()); if (r.baseType != null && @@ -445,30 +569,34 @@ public void preprocessOpenAPI(OpenAPI openAPI) { } } - List formParameters = fromRequestBodyToFormParameters(requestBody, imports); - for (CodegenParameter parameter : formParameters) { - String reference = ""; - if (parameter.isModel) { - Schema nestedSchema = ModelUtils.getSchema(openAPI, parameter.baseType); - CodegenModel model = fromModel(parameter.paramName, nestedSchema); - reference = generateNestedModelTemplate(model); - if (parameter.dataType.equals("List")) { - reference = "[" + reference + "]"; + // if we have at least one request body example, we do not need to construct these dummies + if (!hasRequestBodyExample) { + List formParameters = fromRequestBodyToFormParameters(requestBody, imports); + for (CodegenParameter parameter : formParameters) { + String reference = ""; + if (parameter.isModel) { + Schema nestedSchema = ModelUtils.getSchema(openAPI, parameter.baseType); + CodegenModel model = fromModel(parameter.paramName, nestedSchema); + reference = generateNestedModelTemplate(model); + if (parameter.dataType.equals("List")) { + reference = "[" + reference + "]"; + } } - } - Parameter k6Parameter; - if (parameter.dataType.equals("File")) { - k6Parameter = new Parameter(parameter.paramName, - "http.file(open(\"/path/to/file.bin\", \"b\"), \"test.bin\")"); - } else { - k6Parameter = new Parameter(parameter.paramName, !reference.isEmpty() ? reference - : getDoubleQuotedString(parameter.dataType.toLowerCase(Locale.ROOT))); - } + Parameter k6Parameter; + if (parameter.dataType.equals("File")) { + k6Parameter = new Parameter(parameter.paramName, + "http.file(open(\"/path/to/file.bin\", \"b\"), \"test.bin\")"); + } else { + k6Parameter = new Parameter(parameter.paramName, !reference.isEmpty() ? reference + : getDoubleQuotedString(parameter.dataType.toLowerCase(Locale.ROOT))); + } - bodyOrFormParams.add(k6Parameter); + bodyOrFormParams.add(k6Parameter); + } } } + String accepts = getAccept(openAPI, operation); String responseType = getDoubleQuotedString(accepts); @@ -510,29 +638,43 @@ public void preprocessOpenAPI(OpenAPI openAPI) { } - pathVariables.put(path, variables); + pathVariables.put(groupName, variables); final HTTPParameters params = new HTTPParameters(null, null, httpParams, null, null, null, null, null, responseType.length() > 0 ? responseType : null); assert params.headers != null; - requests.add(new HTTPRequest(method.toString().toLowerCase(Locale.ROOT), path, + + // check if data needs to be extracted from response of this operation + Optional dataExtract = getDataExtractSubstituteParameter( + dataExtractSubstituteParams, operationId); + + // calculate order for this current request + Integer requestOrder = calculateRequestOrder(operationGroupingOrder, requests.size()); + + requests.put(requestOrder, new HTTPRequest(method.toString().toLowerCase(Locale.ROOT), path, queryParams.size() > 0 ? queryParams : null, - bodyOrFormParams.size() > 0 ? new HTTPBody(bodyOrFormParams) : null, - params.headers.size() > 0 ? params : null, k6Checks.size() > 0 ? k6Checks : null)); + bodyOrFormParams.size() > 0 ? new HTTPBody(bodyOrFormParams) : null, hasRequestBodyExample, + params.headers.size() > 0 ? params : null, k6Checks.size() > 0 ? k6Checks : null, + dataExtract.isPresent() ? dataExtract.get() : null)); } - requestGroups.add(new HTTPRequestGroup(path, pathVariables.get(path), requests)); + + addOrUpdateRequestGroup(requestGroups, groupName, pathVariables.get(groupName), requests); } - for (HTTPRequestGroup requestGroup : requestGroups) { + for (HTTPRequestGroup requestGroup : requestGroups.values()) { for (HTTPRequest request : requestGroup.requests) { if (request.path.contains("/{")) { request.path = request.path.replace("/{", "/${"); } } + + // any variables not defined yet but used for subsequent data extraction must be + // initialized + initializeDataExtractSubstituteParameters(dataExtractSubstituteParams, requestGroup); } - additionalProperties.put("requestGroups", requestGroups); + additionalProperties.put("requestGroups", requestGroups.values()); additionalProperties.put("extra", extraParameters); for (String[] supportingTemplateFile : JAVASCRIPT_SUPPORTING_FILES) { @@ -744,9 +886,282 @@ private static String getAccept(OpenAPI openAPI, Operation operation) { return accepts; } - + @Override protected ImmutableMap.Builder addMustacheLambdas() { return super.addMustacheLambdas().put("handleParamValue", new ParameterValueLambda()); } + + /** + * We're iterating over paths but grouping requests across paths, therefore + * these need to be aggregated. + * + * @param requestGroups + * @param groupName + * @param variables + * @param requests + */ + private void addOrUpdateRequestGroup(Map requestGroups, String groupName, + Set variables, Map requests) { + if (requestGroups.containsKey(groupName)) { + HTTPRequestGroup existingHTTPRequestGroup = requestGroups.get(groupName); + existingHTTPRequestGroup.addRequests(requests); + existingHTTPRequestGroup.addVariables(variables); + } else { + requestGroups.put(groupName, new HTTPRequestGroup(groupName, variables, requests)); + } + } + + /** + * If `X_OPERATION_DATAEXTRACT` K6 vendor extension is present, extract info + * from it. + * + * @param openAPI + * @return + */ + private Map getDataExtractSubstituteParameters(OpenAPI openAPI) { + Map dataExtractSubstituteParams = new HashMap<>(); + + for (String path : openAPI.getPaths().keySet()) { + for (Map.Entry methodOperation : openAPI.getPaths().get(path) + .readOperationsMap().entrySet()) { + + final PathItem.HttpMethod method = methodOperation.getKey(); + final Operation operation = methodOperation.getValue(); + final CodegenOperation cgOperation = super.fromOperation(path, method.name(), operation, null); + + if (cgOperation.getHasVendorExtensions() + && cgOperation.vendorExtensions.containsKey(X_OPERATION_DATAEXTRACT) + && cgOperation.vendorExtensions.get(X_OPERATION_DATAEXTRACT) instanceof java.util.Map) { + + Optional dataExtractSubstituteParameter = getDataExtractSubstituteParameter( + (Map) cgOperation.vendorExtensions.get(X_OPERATION_DATAEXTRACT)); + + // TODO: add support for extracting data for multiple params + if (dataExtractSubstituteParameter.isPresent()) { + dataExtractSubstituteParams.put(dataExtractSubstituteParameter.get().operationId, + dataExtractSubstituteParameter.get()); + } + } + + } + } + + return dataExtractSubstituteParams; + } + + /** + * Optionally, retrieve information specified in the `X_OPERATION_DATAEXTRACT` + * K6 vendor extension + * + * @param xOperationDataExtractProperties + * @return optional as only returned if all required info is present + */ + private Optional getDataExtractSubstituteParameter( + Map xOperationDataExtractProperties) { + + Optional operationId = Optional.empty(); + Optional valuePath = Optional.empty(); + Optional parameterName = Optional.empty(); + + for (Map.Entry xOperationDataExtractPropertiesEntry : xOperationDataExtractProperties.entrySet()) { + + switch (String.valueOf(xOperationDataExtractPropertiesEntry.getKey())) { + case X_OPERATION_DATAEXTRACT_OPERATION_ID: + operationId = Optional.of(String.valueOf(xOperationDataExtractPropertiesEntry.getValue())); + continue; + + case X_OPERATION_DATAEXTRACT_VALUE_PATH: + valuePath = Optional.of(String.valueOf(xOperationDataExtractPropertiesEntry.getValue())); + continue; + + case X_OPERATION_DATAEXTRACT_PARAMETER_NAME: + parameterName = Optional.of(String.valueOf(xOperationDataExtractPropertiesEntry.getValue())); + continue; + } + } + + if (operationId.isPresent() && valuePath.isPresent() && parameterName.isPresent()) { + return Optional + .of(new DataExtractSubstituteParameter(operationId.get(), valuePath.get(), parameterName.get())); + + } else { + return Optional.empty(); + } + } + + /** + * Optionally, retrieve data extraction properties for given operation + * + * @param dataExtractSubstituteParams + * @param operationId + * @return optional as only returned if present for given operation + */ + private Optional getDataExtractSubstituteParameter( + Map dataExtractSubstituteParams, String operationId) { + + return (!dataExtractSubstituteParams.isEmpty() && dataExtractSubstituteParams.containsKey(operationId)) + ? Optional.of(dataExtractSubstituteParams.get(operationId)) + : Optional.empty(); + } + + /** + * Optionally, retrieve information specified in the `X_OPERATION_GROUPING` K6 + * vendor extension + * + * @param cgOperation + * @return optional as only returned if required info is present + */ + private Optional extractOperationGrouping(CodegenOperation cgOperation) { + Optional operationGrouping = Optional.empty(); + + if (cgOperation.getHasVendorExtensions() && cgOperation.vendorExtensions.containsKey(X_OPERATION_GROUPING) + && cgOperation.vendorExtensions.get(X_OPERATION_GROUPING) instanceof java.util.Map) { + + Map.Entry operationGroupingEntry = ((Map) cgOperation.vendorExtensions + .get(X_OPERATION_GROUPING)).entrySet().stream().findFirst().get(); + + return Optional.of(new OperationGrouping(String.valueOf(operationGroupingEntry.getKey()), + Integer.parseInt(String.valueOf(operationGroupingEntry.getValue())))); + } + + return operationGrouping; + } + + /** + * If `X_OPERATION_RESPONSE` K6 vendor extension is present, check if given + * operation response should be hidden. + * + * @param resp + * @return true if should be hidden, false otherwise + */ + private boolean shouldHideOperationResponse(ApiResponse resp) { + boolean hideOperationResponse = false; + + if (Objects.nonNull(resp.getExtensions()) && !resp.getExtensions().isEmpty() + && resp.getExtensions().containsKey(X_OPERATION_RESPONSE)) { + + Map respExtensions = (Map) resp.getExtensions().get(X_OPERATION_RESPONSE); + + for (Map.Entry respExtensionEntry : respExtensions.entrySet()) { + + switch (String.valueOf(respExtensionEntry.getKey())) { + case X_OPERATION_RESPONSE_HIDE: + hideOperationResponse = Boolean.valueOf(respExtensionEntry.getValue().toString()); + continue; + } + } + } + + return hideOperationResponse; + } + + /** + * Check if example is present for given request body and content type. + * + * @param requestBody + * @param contentTypeValue + * @return true if present, false otherwise + */ + private boolean hasRequestBodyExample(RequestBody requestBody, String contentTypeValue) { + return (Objects.nonNull(requestBody.getContent()) && requestBody.getContent().containsKey(contentTypeValue) + && Objects.nonNull(requestBody.getContent().get(contentTypeValue).getExamples()) + && !requestBody.getContent().get(contentTypeValue).getExamples().isEmpty()); + } + + /** + * Extract example for given request body. + * + * @param requestBody + * @param contentTypeValue + * @param bodyOrFormParams + */ + private void extractRequestBodyExample(RequestBody requestBody, String contentTypeValue, + List bodyOrFormParams) { + + Optional> requestBodyExampleEntry = requestBody.getContent().get(contentTypeValue) + .getExamples().entrySet().stream().findFirst(); + + if (requestBodyExampleEntry.isPresent()) { + + Example requestBodyExample = requestBodyExampleEntry.get().getValue(); + + try { + JsonNode requestBodyExampleValueJsonNode = Json.mapper() + .readTree(String.valueOf(requestBodyExample.getValue())); + + Iterator> fields = requestBodyExampleValueJsonNode.fields(); + while (fields.hasNext()) { + Map.Entry fieldsEntry = fields.next(); + + JsonNode exampleValueAsJsonNode = fieldsEntry.getValue(); + + Parameter k6Parameter = new Parameter(fieldsEntry.getKey(), + exampleValueAsJsonNode.isNumber() ? exampleValueAsJsonNode.asText() + : exampleValueAsJsonNode.toString()); + + bodyOrFormParams.add(k6Parameter); + } + + } catch (JsonProcessingException e) { + LOGGER.error(e.getMessage(), e); + } + } + } + + /** + * Calculate order for this current request + * + * @param operationGroupingOrder + * @param requests + * @return request order + */ + private Integer calculateRequestOrder(OptionalInt operationGroupingOrder, int requestsSize) { + int requestOrder = 0; + + if (operationGroupingOrder.isPresent()) { + requestOrder = Integer.valueOf(operationGroupingOrder.getAsInt() - 1); + + } else { + switch (requestsSize) { + case 0: + case 1: + requestOrder = requestsSize; + break; + + default: + requestOrder = (requestsSize - 1); + break; + } + } + + return Integer.valueOf(requestOrder); + } + + // + /** + * Any variables not defined yet but used for subsequent data extraction must be + * initialized + * + * @param dataExtractSubstituteParams + * @param requestGroup + */ + private void initializeDataExtractSubstituteParameters( + Map dataExtractSubstituteParams, HTTPRequestGroup requestGroup) { + + if (!dataExtractSubstituteParams.isEmpty()) { + List existingVariablesNames = requestGroup.variables.stream().map(v -> v.key) + .collect(Collectors.toList()); + + Set initializeVariables = dataExtractSubstituteParams.values().stream() + .filter(p -> !existingVariablesNames.contains(toVarName(p.paramName))).collect(Collectors.toSet()); + + if (!initializeVariables.isEmpty()) { + for (DataExtractSubstituteParameter initializeVariable : initializeVariables) { + requestGroup.variables.add(new Parameter(toVarName(initializeVariable.paramName), true)); + } + } + } + } + } diff --git a/modules/openapi-generator/src/main/resources/k6/script.mustache b/modules/openapi-generator/src/main/resources/k6/script.mustache index cac3381de01f..4138e10a0294 100644 --- a/modules/openapi-generator/src/main/resources/k6/script.mustache +++ b/modules/openapi-generator/src/main/resources/k6/script.mustache @@ -19,47 +19,52 @@ export default function() { let {{{key}}} = {{#lambda.handleParamValue}}{{value}}{{/lambda.handleParamValue}} {{/variables}} {{#requests}} - {{#-first}} - let url = BASE_URL + `{{{path}}}{{=<% %>=}}<%#query%><%#-first%>?<%/-first%><%& key%>=<%& value%><%^-last%>&<%/-last%><%/query%><%={{ }}=%>`; + // Request No. {{-index}} - {{#body}} - // TODO: edit the parameters of the request body. - let body = {{#body}}{{=<% %>=}}{<%#parameters%>"<%& key%>": <%& value%><%^-last%>, <%/-last%><%/parameters%>}<%={{ }}=%>{{/body}}; - {{/body}} - {{#params}} - let params = {{#params}}{{=<% %>=}}{headers: {<%# headers%>"<%& key%>": <%& value%><%^-last%>, <%/-last%><%/headers%><%#responseType%>, "Accept": <%& responseType%><%/responseType%>}<%# auth%>, auth: "<%& auth%>"<%/auth%>}<%={{ }}=%>{{/params}}; - {{/params}} - let request = http.{{method}}(url{{#body}}, body{{/body}}{{#params}}, params{{/params}}); - {{#k6Checks}} - {{=<% %>=}} - check(request, { + { + let url = BASE_URL + `{{{path}}}{{=<% %>=}}<%#query%><%#-first%>?<%/-first%><%& key%>=<%& value%><%^-last%>&<%/-last%><%/query%><%={{ }}=%>`; + {{#body}} + {{^hasBodyExample}} + // TODO: edit the parameters of the request body. + {{/hasBodyExample}} + let body = {{#body}}{{=<% %>=}}{<%#parameters%>"<%& key%>": <%& value%><%^-last%>, <%/-last%><%/parameters%>}<%={{ }}=%>{{/body}}; + {{/body}} + + {{#params}} + let params = {{#params}}{{=<% %>=}}{headers: {<%# headers%>"<%& key%>": <%& value%><%^-last%>, <%/-last%><%/headers%><%#responseType%>, "Accept": <%& responseType%><%/responseType%>}<%# auth%>, auth: "<%& auth%>"<%/auth%>}<%={{ }}=%>{{/params}}; + {{/params}} + + {{#isDelete}} + {{#params}} + // this is a DELETE method request - if params are also set, empty body must be passed + let request = http.{{method}}(url, {} {{#params}}, params{{/params}}); + {{/params}} + {{^params}} + let request = http.{{method}}(url); + {{/params}} + {{/isDelete}} + + {{^isDelete}} + let request = http.{{method}}(url{{#body}}, JSON.stringify(body){{/body}}{{#params}}, params{{/params}}); + {{/isDelete}} + + {{#k6Checks}} + {{=<% %>=}} + check(request, { "<%& description%>": (r) => r.status === <%& status%> - }); - <%={{ }}=%> - {{/k6Checks}} - {{/-first}} - {{^-first}} - // Request No. {{-index}} - {{#body}} - // TODO: edit the parameters of the request body. - body = {{#body}}{{=<% %>=}}{<%#parameters%>"<%& key%>": <%& value%><%^-last%>, <%/-last%><%/parameters%>}<%={{ }}=%>{{/body}}; - {{/body}} - {{#params}} - params = {{#params}}{{=<% %>=}}{headers: {<%# headers%>"<%& key%>": <%& value%><%^-last%>, <%/-last%><%/headers%>}<%# auth%>, auth: "<%& auth%>"<%/auth%>}<%={{ }}=%>{{/params}}; - {{/params}} - request = http.{{method}}(url{{#body}}, body{{/body}}{{#params}}, params{{/params}}); - {{#k6Checks}} - {{=<% %>=}} - check(request, { - "<%& description%>": (r) => r.status === <%& status%> - }); - <%={{ }}=%> - {{/k6Checks}} - {{/-first}} - sleep(SLEEP_DURATION); - {{^-last}} - - {{/-last}} + }); + <%={{ }}=%> + {{/k6Checks}} + + {{#dataExtract}} + {{{paramName}}} = JSON.parse(request.body).{{{valuePath}}}; // extract data for subsequent use + {{/dataExtract}} + + {{^-last}} + sleep(SLEEP_DURATION); + {{/-last}} + } + {{/requests}} }); {{/requestGroups}} diff --git a/pom.xml b/pom.xml index bca552e102dc..785c15a82e63 100644 --- a/pom.xml +++ b/pom.xml @@ -1544,6 +1544,7 @@ 1.4 2.11.0 3.12.0 + 1.9 1.3.0 1.0.2 4.9.10 diff --git a/samples/client/petstore/k6/.openapi-generator/FILES b/samples/client/petstore/k6/.openapi-generator/FILES new file mode 100644 index 000000000000..343cd1bd1f83 --- /dev/null +++ b/samples/client/petstore/k6/.openapi-generator/FILES @@ -0,0 +1,2 @@ +README.md +script.js diff --git a/samples/client/petstore/k6/script.js b/samples/client/petstore/k6/script.js index 6988d6417da5..966e67a6132a 100644 --- a/samples/client/petstore/k6/script.js +++ b/samples/client/petstore/k6/script.js @@ -23,182 +23,335 @@ let apiKey = "TODO_EDIT_THE_API_KEY"; export default function() { group("/pet", () => { - let url = BASE_URL + `/pet`; + // Request No. 1 - // TODO: edit the parameters of the request body. - let body = {"id": "long", "category": {"id": "long", "name": "string"}, "name": "string", "photoUrls": "list", "tags": [{"id": "long", "name": "string"}], "status": "string"}; - let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; - let request = http.put(url, body, params); - sleep(SLEEP_DURATION); - + { + let url = BASE_URL + `/pet`; + // TODO: edit the parameters of the request body. + let body = {"id": "long", "category": {"id": "long", "name": "string"}, "name": "string", "photoUrls": "list", "tags": "list", "status": "string"}; + + let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; + + + let request = http.put(url, JSON.stringify(body), params); + + + + sleep(SLEEP_DURATION); + } + + // Request No. 2 - // TODO: edit the parameters of the request body. - body = {"id": "long", "category": {"id": "long", "name": "string"}, "name": "string", "photoUrls": "list", "tags": [{"id": "long", "name": "string"}], "status": "string"}; - params = {headers: {"Content-Type": "application/json"}}; - request = http.post(url, body, params); - sleep(SLEEP_DURATION); + { + let url = BASE_URL + `/pet`; + // TODO: edit the parameters of the request body. + let body = {"id": "long", "category": {"id": "long", "name": "string"}, "name": "string", "photoUrls": "list", "tags": "list", "status": "string"}; + + let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; + + + let request = http.post(url, JSON.stringify(body), params); + + + + } + }); - group("/pet/findByStatus", () => { - let status = 'TODO_EDIT_THE_STATUS'; // specify value as there is no example value for this parameter in OpenAPI spec - let url = BASE_URL + `/pet/findByStatus?status=${status}`; + group("/user/{username}", () => { + let username = 'TODO_EDIT_THE_USERNAME'; // specify value as there is no example value for this parameter in OpenAPI spec + // Request No. 1 - let request = http.get(url); - check(request, { + { + let url = BASE_URL + `/user/${username}`; + + + + let request = http.get(url); + + check(request, { "successful operation": (r) => r.status === 200 - }); - sleep(SLEEP_DURATION); + }); + + + sleep(SLEEP_DURATION); + } + + + // Request No. 2 + { + let url = BASE_URL + `/user/${username}`; + + + let request = http.del(url); + + + + + } + }); - group("/pet/findByTags", () => { - let tags = 'TODO_EDIT_THE_TAGS'; // specify value as there is no example value for this parameter in OpenAPI spec - let url = BASE_URL + `/pet/findByTags?tags=${tags}`; + group("/pet/findByStatus", () => { + let status = 'TODO_EDIT_THE_STATUS'; // specify value as there is no example value for this parameter in OpenAPI spec + // Request No. 1 - let request = http.get(url); - check(request, { + { + let url = BASE_URL + `/pet/findByStatus?status=${status}`; + + + + let request = http.get(url); + + check(request, { "successful operation": (r) => r.status === 200 - }); - sleep(SLEEP_DURATION); + }); + + + } + }); - group("/pet/{petId}", () => { - let petId = 'TODO_EDIT_THE_PETID'; // specify value as there is no example value for this parameter in OpenAPI spec - let url = BASE_URL + `/pet/${petId}`; + group("/user/createWithList", () => { + // Request No. 1 - let request = http.get(url); - check(request, { + { + let url = BASE_URL + `/user/createWithList`; + + let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; + + + let request = http.post(url, params); + + check(request, { "successful operation": (r) => r.status === 200 - }); - sleep(SLEEP_DURATION); - - // Request No. 2 - // TODO: edit the parameters of the request body. - body = {"name": "string", "status": "string"}; - params = {headers: {"Content-Type": "application/x-www-form-urlencoded"}}; - request = http.post(url, body, params); - sleep(SLEEP_DURATION); - - // Request No. 3 - params = {headers: {"api_key": `${apiKey}`}}; - request = http.del(url, params); - sleep(SLEEP_DURATION); + }); + + + } + }); group("/pet/{petId}/uploadImage", () => { let petId = 'TODO_EDIT_THE_PETID'; // specify value as there is no example value for this parameter in OpenAPI spec - let url = BASE_URL + `/pet/${petId}/uploadImage`; + // Request No. 1 - // TODO: edit the parameters of the request body. - let body = {"additionalMetadata": "string", "file": http.file(open("/path/to/file.bin", "b"), "test.bin")}; - let params = {headers: {"Content-Type": "multipart/form-data", "Accept": "application/json"}}; - let request = http.post(url, body, params); - check(request, { + { + let url = BASE_URL + `/pet/${petId}/uploadImage`; + // TODO: edit the parameters of the request body. + let body = {"additionalMetadata": "string", "file": http.file(open("/path/to/file.bin", "b"), "test.bin")}; + + let params = {headers: {"Content-Type": "multipart/form-data", "Accept": "application/json"}}; + + + let request = http.post(url, JSON.stringify(body), params); + + check(request, { "successful operation": (r) => r.status === 200 - }); - sleep(SLEEP_DURATION); + }); + + + } + }); group("/store/inventory", () => { - let url = BASE_URL + `/store/inventory`; + // Request No. 1 - let request = http.get(url); - check(request, { + { + let url = BASE_URL + `/store/inventory`; + + + + let request = http.get(url); + + check(request, { "successful operation": (r) => r.status === 200 - }); - sleep(SLEEP_DURATION); + }); + + + } + }); - group("/store/order", () => { - let url = BASE_URL + `/store/order`; - // Request No. 1 - // TODO: edit the parameters of the request body. - let body = {"id": "long", "petId": "long", "quantity": "integer", "shipDate": "date", "status": "string", "complete": "boolean"}; - let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; - let request = http.post(url, body, params); - check(request, { - "successful operation": (r) => r.status === 200 - }); - sleep(SLEEP_DURATION); - }); - group("/store/order/{orderId}", () => { - let orderId = 'TODO_EDIT_THE_ORDERID'; // specify value as there is no example value for this parameter in OpenAPI spec - let url = BASE_URL + `/store/order/${orderId}`; + group("/user/login", () => { + let password = 'TODO_EDIT_THE_PASSWORD'; // specify value as there is no example value for this parameter in OpenAPI spec + let username = 'TODO_EDIT_THE_USERNAME'; // specify value as there is no example value for this parameter in OpenAPI spec + // Request No. 1 - let request = http.get(url); - check(request, { + { + let url = BASE_URL + `/user/login?username=${username}&password=${password}`; + + + + let request = http.get(url); + + check(request, { "successful operation": (r) => r.status === 200 - }); - sleep(SLEEP_DURATION); - - // Request No. 2 - request = http.del(url); - sleep(SLEEP_DURATION); + }); + + + } + }); group("/user", () => { - let url = BASE_URL + `/user`; + // Request No. 1 - // TODO: edit the parameters of the request body. - let body = {"id": "long", "username": "string", "firstName": "string", "lastName": "string", "email": "string", "password": "string", "phone": "string", "userStatus": "integer"}; - let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; - let request = http.post(url, body, params); - check(request, { + { + let url = BASE_URL + `/user`; + // TODO: edit the parameters of the request body. + let body = {"id": "long", "username": "string", "firstName": "string", "lastName": "string", "email": "string", "password": "string", "phone": "string", "userStatus": "integer"}; + + let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; + + + let request = http.post(url, JSON.stringify(body), params); + + check(request, { "successful operation": (r) => r.status === 200 - }); - sleep(SLEEP_DURATION); + }); + + + } + }); group("/user/createWithArray", () => { - let url = BASE_URL + `/user/createWithArray`; + // Request No. 1 - let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; - let request = http.post(url, params); - check(request, { + { + let url = BASE_URL + `/user/createWithArray`; + + let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; + + + let request = http.post(url, params); + + check(request, { "successful operation": (r) => r.status === 200 - }); - sleep(SLEEP_DURATION); + }); + + + } + }); - group("/user/createWithList", () => { - let url = BASE_URL + `/user/createWithList`; + group("/pet/findByTags", () => { + let tags = 'TODO_EDIT_THE_TAGS'; // specify value as there is no example value for this parameter in OpenAPI spec + // Request No. 1 - let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; - let request = http.post(url, params); - check(request, { + { + let url = BASE_URL + `/pet/findByTags?tags=${tags}`; + + + + let request = http.get(url); + + check(request, { "successful operation": (r) => r.status === 200 - }); - sleep(SLEEP_DURATION); + }); + + + } + }); - group("/user/login", () => { - let password = 'TODO_EDIT_THE_PASSWORD'; // specify value as there is no example value for this parameter in OpenAPI spec - let username = 'TODO_EDIT_THE_USERNAME'; // specify value as there is no example value for this parameter in OpenAPI spec - let url = BASE_URL + `/user/login?username=${username}&password=${password}`; + group("/store/order", () => { + // Request No. 1 - let request = http.get(url); - check(request, { + { + let url = BASE_URL + `/store/order`; + // TODO: edit the parameters of the request body. + let body = {"id": "long", "petId": "long", "quantity": "integer", "shipDate": "date", "status": "string", "complete": "boolean"}; + + let params = {headers: {"Content-Type": "application/json", "Accept": "application/json"}}; + + + let request = http.post(url, JSON.stringify(body), params); + + check(request, { "successful operation": (r) => r.status === 200 - }); - sleep(SLEEP_DURATION); + }); + + + } + }); group("/user/logout", () => { - let url = BASE_URL + `/user/logout`; + // Request No. 1 - let request = http.get(url); - check(request, { + { + let url = BASE_URL + `/user/logout`; + + + + let request = http.get(url); + + check(request, { "successful operation": (r) => r.status === 200 - }); - sleep(SLEEP_DURATION); + }); + + + } + }); - group("/user/{username}", () => { - let username = 'TODO_EDIT_THE_USERNAME'; // specify value as there is no example value for this parameter in OpenAPI spec - let url = BASE_URL + `/user/${username}`; + group("/pet/{petId}", () => { + let petId = 'TODO_EDIT_THE_PETID'; // specify value as there is no example value for this parameter in OpenAPI spec + // Request No. 1 - let request = http.get(url); - check(request, { + { + let url = BASE_URL + `/pet/${petId}`; + + + + let request = http.get(url); + + check(request, { "successful operation": (r) => r.status === 200 - }); - sleep(SLEEP_DURATION); - + }); + + + sleep(SLEEP_DURATION); + } + + // Request No. 2 - // TODO: edit the parameters of the request body. - body = {"id": "long", "username": "string", "firstName": "string", "lastName": "string", "email": "string", "password": "string", "phone": "string", "userStatus": "integer"}; - params = {headers: {"Content-Type": "application/json"}}; - request = http.put(url, body, params); - sleep(SLEEP_DURATION); - - // Request No. 3 - request = http.del(url); - sleep(SLEEP_DURATION); + { + let url = BASE_URL + `/pet/${petId}`; + + let params = {headers: {"api_key": `${apiKey}`, "Accept": "application/json"}}; + + // this is a DELETE method request - if params are also set, empty body must be passed + let request = http.del(url, {} , params); + + + + + } + + }); + group("/store/order/{orderId}", () => { + let orderId = 'TODO_EDIT_THE_ORDERID'; // specify value as there is no example value for this parameter in OpenAPI spec + + // Request No. 1 + { + let url = BASE_URL + `/store/order/${orderId}`; + + + + let request = http.get(url); + + check(request, { + "successful operation": (r) => r.status === 200 + }); + + + sleep(SLEEP_DURATION); + } + + + // Request No. 2 + { + let url = BASE_URL + `/store/order/${orderId}`; + + + let request = http.del(url); + + + + + } + }); }