Skip to content

Commit

Permalink
feat(#1175): added random generator framework
Browse files Browse the repository at this point in the history
Framework was added in favour of OpenApiTestDataGenerator implementation
  • Loading branch information
Thorsten Schlathoelter authored and bbortt committed Nov 24, 2024
1 parent b808495 commit a8c6219
Show file tree
Hide file tree
Showing 43 changed files with 2,669 additions and 1,106 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ private static String createValidationExpression(OasSchema schema) {
}

switch (schema.type) {
case "string" :
case OpenApiConstants.TYPE_STRING :
if (schema.format != null && schema.format.equals("date")) {
return "@matchesDatePattern('yyyy-MM-dd')@";
} else if (schema.format != null && schema.format.equals("date-time")) {
Expand All @@ -190,7 +190,7 @@ private static String createValidationExpression(OasSchema schema) {
}
case OpenApiConstants.TYPE_NUMBER, OpenApiConstants.TYPE_INTEGER:
return "@isNumber()@";
case "boolean" :
case OpenApiConstants.TYPE_BOOLEAN :
return "@matches(true|false)@";
default:
return "@ignore@";
Expand Down Expand Up @@ -219,25 +219,25 @@ public static String createValidationRegex(@Nullable OasSchema schema) {
}

switch (schema.type) {
case "string" :
case OpenApiConstants.TYPE_STRING:
if (schema.format != null && schema.format.equals("date")) {
return "\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01])";
} else if (schema.format != null && schema.format.equals("date-time")) {
return "\\d{4}-\\d{2}-\\d{2}T[01]\\d:[0-5]\\d:[0-5]\\dZ";
} else if (hasText(schema.pattern)) {
return schema.pattern;
} else if (!isEmpty(schema.enum_)) {
return "(" + (String.join("|", schema.enum_)) + ")";
return "(" + String.join("|", schema.enum_) + ")";
} else if (schema.format != null && schema.format.equals("uuid")) {
return "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
} else {
return ".*";
}
case OpenApiConstants.TYPE_NUMBER:
return "[0-9]+\\.?[0-9]*";
case "integer" :
case OpenApiConstants.TYPE_INTEGER:
return "[0-9]+";
case "boolean" :
case OpenApiConstants.TYPE_BOOLEAN:
return "(true|false)";
default:
return "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ private void buildMessageFromOperation(OperationPathAdapter operationPathAdapter
if (context.getVariables().containsKey(parameter.getName())) {
parameterValue = "\\" + CitrusSettings.VARIABLE_PREFIX + parameter.getName() + CitrusSettings.VARIABLE_SUFFIX;
} else {
parameterValue = OpenApiTestDataGenerator.createRandomValueExpression((OasSchema) parameter.schema, false);
parameterValue = OpenApiTestDataGenerator.createRandomValueExpression((OasSchema) parameter.schema);
}
randomizedPath = Pattern.compile("\\{" + parameter.getName() + "}")
.matcher(randomizedPath)
Expand All @@ -151,8 +151,7 @@ private void buildMessageFromOperation(OperationPathAdapter operationPathAdapter
private void setSpecifiedBody(TestContext context, OasOperation operation) {
Optional<OasSchema> body = OasModelHelper.getRequestBodySchema(
openApiSpec.getOpenApiDoc(context), operation);
body.ifPresent(oasSchema -> httpMessage.setPayload(OpenApiTestDataGenerator.createOutboundPayload(oasSchema,
OasModelHelper.getSchemaDefinitions(openApiSpec.getOpenApiDoc(context)), openApiSpec)));
body.ifPresent(oasSchema -> httpMessage.setPayload(OpenApiTestDataGenerator.createOutboundPayload(oasSchema, openApiSpec)));
}

private void setSpecifiedQueryParameters(TestContext context, OasOperation operation) {
Expand All @@ -179,9 +178,7 @@ private void setSpecifiedHeaders(TestContext context, OasOperation operation) {
.forEach(param -> {
if(httpMessage.getHeader(param.getName()) == null && !configuredHeaders.contains(param.getName())) {
httpMessage.setHeader(param.getName(),
OpenApiTestDataGenerator.createRandomValueExpression(param.getName(), (OasSchema) param.schema,
OasModelHelper.getSchemaDefinitions(openApiSpec.getOpenApiDoc(
context)), false, openApiSpec, context));
OpenApiTestDataGenerator.createRandomValueExpression(param.getName(), (OasSchema) param.schema, openApiSpec, context));
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE;

import io.apicurio.datamodels.openapi.models.OasDocument;
import io.apicurio.datamodels.openapi.models.OasOperation;
import io.apicurio.datamodels.openapi.models.OasResponse;
import io.apicurio.datamodels.openapi.models.OasSchema;
Expand Down Expand Up @@ -160,26 +159,24 @@ public Message build(TestContext context, String messageType) {
}

private void fillRandomData(OperationPathAdapter operationPathAdapter, TestContext context) {
OasDocument oasDocument = openApiSpec.getOpenApiDoc(context);

if (operationPathAdapter.operation().responses != null) {
buildResponse(context, operationPathAdapter.operation(), oasDocument);
buildResponse(context, operationPathAdapter.operation());
}
}

private void buildResponse(TestContext context, OasOperation operation,
OasDocument oasDocument) {
private void buildResponse(TestContext context, OasOperation operation) {

Optional<OasResponse> responseForRandomGeneration = OasModelHelper.getResponseForRandomGeneration(
openApiSpec.getOpenApiDoc(context), operation, statusCode, null);

if (responseForRandomGeneration.isPresent()) {
buildRandomHeaders(context, oasDocument, responseForRandomGeneration.get());
buildRandomPayload(operation, oasDocument, responseForRandomGeneration.get());
buildRandomHeaders(context, responseForRandomGeneration.get());
buildRandomPayload(operation, responseForRandomGeneration.get());
}
}

private void buildRandomHeaders(TestContext context, OasDocument oasDocument, OasResponse response) {
private void buildRandomHeaders(TestContext context, OasResponse response) {
Set<String> filteredHeaders = new HashSet<>(getMessage().getHeaders().keySet());
Predicate<Entry<String, OasSchema>> filteredHeadersPredicate = entry -> !filteredHeaders.contains(
entry.getKey());
Expand All @@ -191,7 +188,6 @@ private void buildRandomHeaders(TestContext context, OasDocument oasDocument, Oa
.forEach(entry -> addHeaderBuilder(new DefaultHeaderBuilder(
singletonMap(entry.getKey(), createRandomValueExpression(entry.getKey(),
entry.getValue(),
OasModelHelper.getSchemaDefinitions(oasDocument), false,
openApiSpec,
context))))
);
Expand All @@ -209,8 +205,7 @@ private void buildRandomHeaders(TestContext context, OasDocument oasDocument, Oa
+ CitrusSettings.VARIABLE_SUFFIX)))));
}

private void buildRandomPayload(OasOperation operation, OasDocument oasDocument,
OasResponse response) {
private void buildRandomPayload(OasOperation operation, OasResponse response) {

Optional<OasAdapter<OasSchema, String>> schemaForMediaTypeOptional;
if (statusCode.startsWith("2")) {
Expand All @@ -227,7 +222,7 @@ private void buildRandomPayload(OasOperation operation, OasDocument oasDocument,
OasAdapter<OasSchema, String> schemaForMediaType = schemaForMediaTypeOptional.get();
if (getMessage().getPayload() == null || (
getMessage().getPayload() instanceof String string && string.isEmpty())) {
createRandomPayload(getMessage(), oasDocument, schemaForMediaType);
createRandomPayload(getMessage(), schemaForMediaType);
}

// If we have a schema and a media type and the content type has not yet been set, do it.
Expand All @@ -238,21 +233,19 @@ private void buildRandomPayload(OasOperation operation, OasDocument oasDocument,
}
}

private void createRandomPayload(HttpMessage message, OasDocument oasDocument, OasAdapter<OasSchema, String> schemaForMediaType) {
private void createRandomPayload(HttpMessage message, OasAdapter<OasSchema, String> schemaForMediaType) {

if (schemaForMediaType.node() == null) {
// No schema means no payload, no type
message.setPayload(null);
} else {
if (TEXT_PLAIN_VALUE.equals(schemaForMediaType.adapted())) {
// Schema but plain text
message.setPayload(createOutboundPayload(schemaForMediaType.node(),
OasModelHelper.getSchemaDefinitions(oasDocument), openApiSpec));
message.setPayload(createOutboundPayload(schemaForMediaType.node(), openApiSpec));
message.setHeader(HttpMessageHeaders.HTTP_CONTENT_TYPE, TEXT_PLAIN_VALUE);
} else if (APPLICATION_JSON_VALUE.equals(schemaForMediaType.adapted())) {
// Json Schema
message.setPayload(createOutboundPayload(schemaForMediaType.node(),
OasModelHelper.getSchemaDefinitions(oasDocument), openApiSpec));
message.setPayload(createOutboundPayload(schemaForMediaType.node(), openApiSpec));
message.setHeader(HttpMessageHeaders.HTTP_CONTENT_TYPE, APPLICATION_JSON_VALUE);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.citrusframework.openapi.random;

import io.apicurio.datamodels.openapi.models.OasSchema;
import java.util.concurrent.ThreadLocalRandom;
import org.citrusframework.openapi.model.OasModelHelper;

/**
* A generator for producing random arrays based on an OpenAPI schema. This class extends the
* {@link RandomGenerator} and provides a specific implementation for generating random arrays
* with constraints defined in the schema.
*
* <p>The generator supports arrays with items of a single schema type. If the array's items have
* different schemas, an {@link UnsupportedOperationException} will be thrown.</p>s
*
*/
public class RandomArrayGenerator extends RandomGenerator {

@Override
public boolean handles(OasSchema other) {
return OasModelHelper.isArrayType(other);
}

@Override
void generate(RandomContext randomContext, OasSchema schema) {
Object items = schema.items;

if (items instanceof OasSchema itemsSchema) {
createRandomArrayValueWithSchemaItem(randomContext, schema, itemsSchema);
} else {
throw new UnsupportedOperationException(
"Random array creation for an array with items having different schema is currently not supported!");
}
}

private static void createRandomArrayValueWithSchemaItem(RandomContext randomContext,
OasSchema schema,
OasSchema itemsSchema) {

Number minItems = schema.minItems != null ? schema.minItems : 1;
Number maxItems = schema.maxItems != null ? schema.maxItems : 10;

int nItems = ThreadLocalRandom.current()
.nextInt(minItems.intValue(), maxItems.intValue() + 1);

randomContext.getRandomModelBuilder().array(() -> {
for (int i = 0; i < nItems; i++) {
randomContext.generate(itemsSchema);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.citrusframework.openapi.random;

import static org.springframework.util.CollectionUtils.isEmpty;

import io.apicurio.datamodels.openapi.models.OasSchema;
import io.apicurio.datamodels.openapi.v3.models.Oas30Schema;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import org.citrusframework.openapi.model.OasModelHelper;

/**
* A generator for producing random composite schemas based on an OpenAPI schema. This class extends
* the {@link RandomGenerator} and provides a specific implementation for generating composite schemas
* with constraints defined in the schema.
*
* <p>The generator supports composite schemas, which include `allOf`, `anyOf`, and `oneOf` constructs.</p>
*/
public class RandomCompositeGenerator extends RandomGenerator {

@Override
public boolean handles(OasSchema other) {
return OasModelHelper.isCompositeSchema(other);
}

@Override
void generate(RandomContext randomContext, OasSchema schema) {

if (!isEmpty(schema.allOf)) {
createAllOff(randomContext, schema);
} else if (schema instanceof Oas30Schema oas30Schema && !isEmpty(oas30Schema.anyOf)) {
createAnyOf(randomContext, oas30Schema);
} else if (schema instanceof Oas30Schema oas30Schema && !isEmpty(oas30Schema.oneOf)) {
createOneOf(randomContext, oas30Schema.oneOf);
}
}

private static void createOneOf(RandomContext randomContext, List<OasSchema> schemas) {
int schemaIndex = ThreadLocalRandom.current().nextInt(schemas.size());
randomContext.getRandomModelBuilder().object(() ->
randomContext.generate(schemas.get(schemaIndex)));
}

private static void createAnyOf(RandomContext randomContext, Oas30Schema schema) {

randomContext.getRandomModelBuilder().object(() -> {
boolean anyAdded = false;
for (OasSchema oneSchema : schema.anyOf) {
if (ThreadLocalRandom.current().nextBoolean()) {
randomContext.generate(oneSchema);
anyAdded = true;
}
}

// Add at least one
if (!anyAdded) {
createOneOf(randomContext, schema.anyOf);
}
});
}

private static Map<String, Object> createAllOff(RandomContext randomContext, OasSchema schema) {
Map<String, Object> allOf = new HashMap<>();

randomContext.getRandomModelBuilder().object(() -> {
for (OasSchema oneSchema : schema.allOf) {
randomContext.generate(oneSchema);
}
});

return allOf;
}
}
Loading

0 comments on commit a8c6219

Please sign in to comment.