Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ K6 Generator ] Support for extracting examples defined at parameter level of Swagger/OpenAPI specification, plus minor fixes #9750

Merged
merged 3 commits into from
Aug 22, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,60 @@

package org.openapitools.codegen.languages;

import static org.openapitools.codegen.utils.StringUtils.camelize;
import static org.openapitools.codegen.utils.StringUtils.dashize;
import static org.openapitools.codegen.utils.StringUtils.underscore;

import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

import org.apache.commons.lang3.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.CodegenParameter;
import org.openapitools.codegen.CodegenProperty;
import org.openapitools.codegen.CodegenResponse;
import org.openapitools.codegen.CodegenType;
import org.openapitools.codegen.DefaultCodegen;
import org.openapitools.codegen.SupportingFile;
import org.openapitools.codegen.meta.GeneratorMetadata;
import org.openapitools.codegen.meta.Stability;
import org.openapitools.codegen.utils.ModelUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ImmutableMap.Builder;
import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Mustache.Lambda;
import com.samskivert.mustache.Template;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.servers.Server;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.*;
import org.openapitools.codegen.meta.GeneratorMetadata;
import org.openapitools.codegen.meta.Stability;
import org.openapitools.codegen.utils.ModelUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.io.File;
import java.util.*;

import static org.openapitools.codegen.utils.StringUtils.*;

public class K6ClientCodegen extends DefaultCodegen implements CodegenConfig {

Expand All @@ -50,30 +82,98 @@ public K6ClientCodegen() {

}

static class Parameter {
String key;
Object value;

public Parameter(String key, Object value) {
this.key = key;
this.value = value;
}

@Override
public int hashCode() {
return key.hashCode();
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
Parameter p = (Parameter) obj;
return key.equals(p.key) && value.equals(p.value);
}
}
static class Parameter {
String key;
Object value;
boolean hasExample;

public Parameter(String key, Object value) {
this.key = key;
this.value = value;
}

public Parameter(String key, Object exampleValue, boolean hasExample) {
this.key = key;
this.value = exampleValue;
this.hasExample = hasExample;
}

@Override
public int hashCode() {
return key.hashCode();
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
Parameter p = (Parameter) obj;
return key.equals(p.key) && value.equals(p.value) && hasExample == p.hasExample;
}
}

static class ParameterValueLambda implements Mustache.Lambda {
private static final String NO_EXAMPLE_PARAM_VALUE_PREFIX = "TODO_EDIT_THE_";

@Override
public void execute(Template.Fragment fragment, Writer writer) throws IOException {

// default used if no example is provided
String noExampleParamValue = String.join("",
quoteExample(
String.join("", NO_EXAMPLE_PARAM_VALUE_PREFIX, fragment.execute())),
";",
" // specify value as there is no example value for this parameter in OpenAPI spec");

// param has example(s)
if (fragment.context() instanceof K6ClientCodegen.Parameter
&& ((K6ClientCodegen.Parameter) fragment.context()).hasExample) {

Object rawValue = ((K6ClientCodegen.Parameter) fragment.context()).value;

// handle as 'examples'
if (rawValue instanceof Map) {

@SuppressWarnings("unchecked")
Set<String> exampleValues = ((Map<String, Example>) rawValue).values().stream()
.map(x -> quoteExample(
StringEscapeUtils.escapeEcmaScript(
String.valueOf(x.getValue()))))
.collect(Collectors.toCollection(() -> new TreeSet<String>()));

if (!exampleValues.isEmpty()) {

writer.write(String.join("",
Arrays.toString(exampleValues.toArray()),
".shift();",
" // first element from list extracted from 'examples' field defined at the parameter level of OpenAPI spec"));

} else {
writer.write(noExampleParamValue);
}

// handle as (single) 'example'
} else {
writer.write(String.join("",
quoteExample(
StringEscapeUtils.escapeEcmaScript(
String.valueOf(
((K6ClientCodegen.Parameter) fragment.context()).value))),
";",
" // extracted from 'example' field defined at the parameter level of OpenAPI spec"));
}

} else {
writer.write(noExampleParamValue);
}
}

private static String quoteExample(String exampleValue) {
return StringUtils.wrap(exampleValue, "'");
}
}

static class HTTPBody {
List<Parameter> parameters;
Expand Down Expand Up @@ -165,7 +265,7 @@ public HTTPRequestGroup(String groupName, Set<Parameter> variables, List<HTTPReq
}
}

private final Logger LOGGER = LoggerFactory.getLogger(JavascriptClientCodegen.class);
private final Logger LOGGER = LoggerFactory.getLogger(K6ClientCodegen.class);

public static final String PROJECT_NAME = "projectName";
public static final String MODULE_NAME = "moduleName";
Expand Down Expand Up @@ -345,8 +445,8 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
}
}

List<CodegenParameter> formParameteres = fromRequestBodyToFormParameters(requestBody, imports);
for (CodegenParameter parameter : formParameteres) {
List<CodegenParameter> formParameters = fromRequestBodyToFormParameters(requestBody, imports);
for (CodegenParameter parameter : formParameters) {
String reference = "";
if (parameter.isModel) {
Schema nestedSchema = ModelUtils.getSchema(openAPI, parameter.baseType);
Expand Down Expand Up @@ -384,7 +484,23 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
case "query":
if (parameter.getIn().equals("query"))
queryParams.add(new Parameter(parameter.getName(), getTemplateVariable(parameter.getName())));
variables.add(new Parameter(toVarName(parameter.getName()), parameter.getName().toUpperCase(Locale.ROOT)));
if (!pathVariables.containsKey(path)) {
// use 'example' field defined at the parameter level of OpenAPI spec
if (Objects.nonNull(parameter.getExample())) {
variables.add(new Parameter(toVarName(parameter.getName()),
parameter.getExample(), true));

// use 'examples' field defined at the parameter level of OpenAPI spec
} else if (Objects.nonNull(parameter.getExamples())) {
variables.add(new Parameter(toVarName(parameter.getName()),
parameter.getExamples(), true));

// no example provided, generated script will contain placeholder value
} else {
variables.add(new Parameter(toVarName(parameter.getName()),
parameter.getName().toUpperCase(Locale.ROOT)));
}
}
break;
default:
break;
Expand Down Expand Up @@ -432,7 +548,6 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
}
}

//
private String generateNestedModelTemplate(CodegenModel model) {
StringBuilder reference = new StringBuilder();
int modelEntrySetSize = model.getAllVars().size();
Expand Down Expand Up @@ -629,4 +744,9 @@ private static String getAccept(OpenAPI openAPI, Operation operation) {

return accepts;
}

@Override
protected Builder<String, Lambda> addMustacheLambdas() {
return super.addMustacheLambdas().put("handleParamValue", new ParameterValueLambda());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ The `script.js` file contains most of the Swagger/OpenAPI specification and you

Global header variables are defined at the top of the file, like `api_key`. Each path in the specification is converted into a [group](https://docs.k6.io/docs/tags-and-groups) in k6 and each group contains all the request methods related to that path. Path and query parameters are extracted from the specification and put at the start of the group. The URL is constructed from the base URL plus path and query.

If the Swagger/OpenAPI specification used as the input spec contains examples at parameter level, those will be extracted and utilized as parameter values. The `handleParamValue` custom Mustache lambda registered for use in the K6 `script.mustache` template handles the conditional checks, formatting, and outputting of parameter values. If a given parameter has value specified – either in `example` or `examples` field, defined at the parameter level – that value will be used. For list (`examples`), entire list will be output in the generated script and the first element from that list will be assigned as parameter value. If a given parameter does not have an example defined, a placeholder value with `TODO_EDIT_THE_` prefix will be generated for that parameter, and you will have to assign a value before you can run the script. In other words, you can now generate K6 test scripts which are ready to run, provided the Swagger/OpenAPI specification used as the input spec contains examples for all of the path/query parameters; see `modules/openapi-generator/src/test/resources/3_0/examples.yaml` for an example of such specification, and https://swagger.io/docs/specification/adding-examples/ for more information about adding examples.

k6 specific parameters are in the [`params`](https://docs.k6.io/docs/params-k6http) object, and `body` contains the [request](https://docs.k6.io/docs/http-requests) body which is in the form of `identifier: type`, which the `type` should be substituted by a proper value. Then goes the request and the check.

[Check](https://docs.k6.io/docs/checks) are like asserts but differ in that they don't halt execution, instead they just store the result of the check, pass or fail, and let the script execution continue.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default function() {
{{#requestGroups}}
group("{{{groupName}}}", () => {
{{#variables}}
let {{{key}}} = "TODO_EDIT_THE_{{{value}}}";
let {{{key}}} = {{#lambda.handleParamValue}}{{value}}{{/lambda.handleParamValue}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this doesn't need the extra indentation.

{{/variables}}
{{#requests}}
{{#-first}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ paths:
in: query
schema:
type: string
example: 'example2 value'
example: 'example2 value'
responses:
'200':
description: successful operation
Expand All @@ -61,7 +61,7 @@ paths:
description: successful operation
/example3/plural:
get:
operationId: example3GetSingular
operationId: example3GetPlural
parameters:
- name: parameter
in: query
Expand Down Expand Up @@ -92,7 +92,7 @@ paths:
description: successful operation
/example4/plural:
post:
operationId: example4PostSingular
operationId: example4PostPlural
requestBody:
content:
application/x-www-form-urlencoded:
Expand Down
2 changes: 1 addition & 1 deletion samples/client/petstore/k6/.openapi-generator/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
5.0.0-SNAPSHOT
5.2.0-SNAPSHOT
2 changes: 2 additions & 0 deletions samples/client/petstore/k6/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ The `script.js` file contains most of the Swagger/OpenAPI specification and you

Global header variables are defined at the top of the file, like `api_key`. Each path in the specification is converted into a [group](https://docs.k6.io/docs/tags-and-groups) in k6 and each group contains all the request methods related to that path. Path and query parameters are extracted from the specification and put at the start of the group. The URL is constructed from the base URL plus path and query.

If the Swagger/OpenAPI specification used as the input spec contains examples at parameter level, those will be extracted and utilized as parameter values. The `handleParamValue` custom Mustache lambda registered for use in the K6 `script.mustache` template handles the conditional checks, formatting, and outputting of parameter values. If a given parameter has value specified – either in `example` or `examples` field, defined at the parameter level – that value will be used. For list (`examples`), entire list will be output in the generated script and the first element from that list will be assigned as parameter value. If a given parameter does not have an example defined, a placeholder value with `TODO_EDIT_THE_` prefix will be generated for that parameter, and you will have to assign a value before you can run the script. In other words, you can now generate K6 test scripts which are ready to run, provided the Swagger/OpenAPI specification used as the input spec contains examples for all of the path/query parameters; see `modules/openapi-generator/src/test/resources/3_0/examples.yaml` for an example of such specification, and https://swagger.io/docs/specification/adding-examples/ for more information about adding examples.

k6 specific parameters are in the [`params`](https://docs.k6.io/docs/params-k6http) object, and `body` contains the [request](https://docs.k6.io/docs/http-requests) body which is in the form of `identifier: type`, which the `type` should be substituted by a proper value. Then goes the request and the check.

[Check](https://docs.k6.io/docs/checks) are like asserts but differ in that they don't halt execution, instead they just store the result of the check, pass or fail, and let the script execution continue.
Expand Down
20 changes: 10 additions & 10 deletions samples/client/petstore/k6/script.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/*
* OpenAPI Petstore
* This is a sample server Petstore server. For this sample, you can use the api key \"special-key\" to test the authorization filters
* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
*
* OpenAPI spec version: 1.0.0
*
* NOTE: This class is auto generated by OpenAPI Generator.
* https://github.com/OpenAPITools/openapi-generator
*
* OpenAPI generator version: 5.0.0-SNAPSHOT
* OpenAPI generator version: 5.2.0-SNAPSHOT
*/


Expand Down Expand Up @@ -39,7 +39,7 @@ export default function() {
sleep(SLEEP_DURATION);
});
group("/pet/findByStatus", () => {
let status = "TODO_EDIT_THE_STATUS";
let status = 'TODO_EDIT_THE_STATUS'; // specify value as there is no example value for this parameter in OpenAPI spec
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extra indentation in the template is reflected here, which I think is unnecessary.

let url = BASE_URL + `/pet/findByStatus?status=${status}`;
// Request No. 1
let request = http.get(url);
Expand All @@ -49,7 +49,7 @@ export default function() {
sleep(SLEEP_DURATION);
});
group("/pet/findByTags", () => {
let tags = "TODO_EDIT_THE_TAGS";
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}`;
// Request No. 1
let request = http.get(url);
Expand All @@ -59,7 +59,7 @@ export default function() {
sleep(SLEEP_DURATION);
});
group("/pet/{petId}", () => {
let petId = "TODO_EDIT_THE_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}`;
// Request No. 1
let request = http.get(url);
Expand All @@ -81,7 +81,7 @@ export default function() {
sleep(SLEEP_DURATION);
});
group("/pet/{petId}/uploadImage", () => {
let petId = "TODO_EDIT_THE_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}/uploadImage`;
// Request No. 1
// TODO: edit the parameters of the request body.
Expand Down Expand Up @@ -115,7 +115,7 @@ export default function() {
sleep(SLEEP_DURATION);
});
group("/store/order/{orderId}", () => {
let orderId = "TODO_EDIT_THE_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}`;
// Request No. 1
let request = http.get(url);
Expand Down Expand Up @@ -161,8 +161,8 @@ export default function() {
sleep(SLEEP_DURATION);
});
group("/user/login", () => {
let password = "TODO_EDIT_THE_PASSWORD";
let username = "TODO_EDIT_THE_USERNAME";
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong, but I expect these values to be filled with examples? Or is it another example script altogether? If so, please regenerate that example alongside this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi

Thank you for the feedback. My IDE does not understand mustache (yet), so apologies for the indentation problems; I’ll fix it ASAP.

Regarding the samples/client/petstore/k6/script.js, please keep in mind samples are generated based on whatever is defined in the config, and for K6 generator, the config file https://github.com/OpenAPITools/openapi-generator/blob/master/bin/configs/other/k6.yaml uses modules/openapi-generator/src/test/resources/2_0/petstore.yaml as the input spec; unfortunately, that OpenAPI spec (modules/openapi-generator/src/test/resources/2_0/petstore.yaml) does not have any examples, and since it’s used by many other generators I did not want to introduce any changes in such place; this is also the reason why in the docs for this PR I provided example (“Generate script based on Swagger/OpenAPI specification with examples”) based on modules/openapi-generator/src/test/resources/3_0/examples.yaml.

Whether modules/openapi-generator/src/test/resources/2_0/petstore.yaml itself should be updated with examples – perhaps that’s a good question to ask the OpenAPI maintainers?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wing328 Any comments?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

modules/openapi-generator/src/test/resources/2_0/petstore.yaml

We usually keep it as it's and create a new one based on it with more edge cases per generator.

let url = BASE_URL + `/user/login?username=${username}&password=${password}`;
// Request No. 1
let request = http.get(url);
Expand All @@ -181,7 +181,7 @@ export default function() {
sleep(SLEEP_DURATION);
});
group("/user/{username}", () => {
let username = "TODO_EDIT_THE_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}`;
// Request No. 1
let request = http.get(url);
Expand Down