Skip to content

Commit

Permalink
fix(#1175): OpenAPI connector enhancements for simulator random messa…
Browse files Browse the repository at this point in the history
…ge generation
  • Loading branch information
Thorsten Schlathoelter committed Jun 11, 2024
1 parent 40e7de2 commit 1b1adfe
Show file tree
Hide file tree
Showing 23 changed files with 1,600 additions and 331 deletions.
6 changes: 6 additions & 0 deletions connectors/citrus-openapi/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.17.0</version>
<scope>compile</scope>
</dependency>
</dependencies>

<profiles>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.citrusframework.openapi;

import java.util.ArrayList;
import java.util.List;
import org.citrusframework.repository.BaseRepository;
import org.citrusframework.spi.Resource;

/**
* OpenApi repository holding a set of OpenApi json schema resources known in the test scope.
* @since 4.4.0
*/
public class OpenApiRepository extends BaseRepository {

private static final String DEFAULT_NAME = "openApiSchemaRepository";

/** List of schema resources */
private final List<OpenApiSpecification> openApiSpecifications = new ArrayList<>();


/** An optional context path, used for each api, without taking into account any {@link OpenApiSpecification} specific context path. */
private String rootContextPath;

public OpenApiRepository() {
super(DEFAULT_NAME);
}

public String getRootContextPath() {
return rootContextPath;
}

public void setRootContextPath(String rootContextPath) {
this.rootContextPath = rootContextPath;
}

@Override
public void addRepository(Resource openApiResource) {

OpenApiSpecification openApiSpecification = OpenApiSpecification.from(openApiResource);
openApiSpecification.setRootContextPath(rootContextPath);

this.openApiSpecifications.add(openApiSpecification);

OpenApiSpecificationProcessor.lookup().values().forEach(processor -> processor.process(openApiSpecification));
}

public List<OpenApiSpecification> getOpenApiSpecifications() {
return openApiSpecifications;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,21 @@
*/
public class OpenApiSpecification {

public static final String HTTPS = "https";
public static final String HTTP = "http";
/** URL to load the OpenAPI specification */
private String specUrl;

private String httpClient;
private String requestUrl;

/**
* The optional root context path to which the OpenAPI is hooked.
* This path is prepended to the base path specified in the OpenAPI configuration.
* If no root context path is specified, only the base path and additional segments are used.
*/
private String rootContextPath;

private OasDocument openApiDoc;

private boolean generateOptionalFields = true;
Expand All @@ -56,7 +65,7 @@ public static OpenApiSpecification from(String specUrl) {
public static OpenApiSpecification from(URL specUrl) {
OpenApiSpecification specification = new OpenApiSpecification();
OasDocument openApiDoc;
if (specUrl.getProtocol().startsWith("https")) {
if (specUrl.getProtocol().startsWith(HTTPS)) {
openApiDoc = OpenApiResourceLoader.fromSecuredWebResource(specUrl);
} else {
openApiDoc = OpenApiResourceLoader.fromWebResource(specUrl);
Expand All @@ -76,11 +85,11 @@ public static OpenApiSpecification from(Resource resource) {
specification.setOpenApiDoc(openApiDoc);

String schemeToUse = Optional.ofNullable(OasModelHelper.getSchemes(openApiDoc))
.orElse(Collections.singletonList("http"))
.orElse(Collections.singletonList(HTTP))
.stream()
.filter(s -> s.equals("http") || s.equals("https"))
.filter(s -> s.equals(HTTP) || s.equals(HTTPS))
.findFirst()
.orElse("http");
.orElse(HTTP);

specification.setSpecUrl(resource.getLocation());
specification.setRequestUrl(String.format("%s://%s%s", schemeToUse, OasModelHelper.getHost(openApiDoc), OasModelHelper.getBasePath(openApiDoc)));
Expand All @@ -102,17 +111,17 @@ public OasDocument getOpenApiDoc(TestContext context) {
resolvedSpecUrl = requestUrl.endsWith("/") ? requestUrl + resolvedSpecUrl.substring(1) : requestUrl + resolvedSpecUrl;
} else if (httpClient != null && context.getReferenceResolver().isResolvable(httpClient, HttpClient.class)) {
String baseUrl = context.getReferenceResolver().resolve(httpClient, HttpClient.class).getEndpointConfiguration().getRequestUrl();
resolvedSpecUrl = baseUrl.endsWith("/") ? baseUrl + resolvedSpecUrl.substring(1) : baseUrl + resolvedSpecUrl;;
resolvedSpecUrl = baseUrl.endsWith("/") ? baseUrl + resolvedSpecUrl.substring(1) : baseUrl + resolvedSpecUrl;
} else {
throw new CitrusRuntimeException(("Failed to resolve OpenAPI spec URL from relative path %s - " +
"make sure to provide a proper base URL when using relative paths").formatted(resolvedSpecUrl));
}
}

if (resolvedSpecUrl.startsWith("http")) {
if (resolvedSpecUrl.startsWith(HTTP)) {
try {
URL specWebResource = new URL(resolvedSpecUrl);
if (resolvedSpecUrl.startsWith("https")) {
if (resolvedSpecUrl.startsWith(HTTPS)) {
openApiDoc = OpenApiResourceLoader.fromSecuredWebResource(specWebResource);
} else {
openApiDoc = OpenApiResourceLoader.fromWebResource(specWebResource);
Expand All @@ -129,11 +138,11 @@ public OasDocument getOpenApiDoc(TestContext context) {

if (requestUrl == null) {
String schemeToUse = Optional.ofNullable(OasModelHelper.getSchemes(openApiDoc))
.orElse(Collections.singletonList("http"))
.orElse(Collections.singletonList(HTTP))
.stream()
.filter(s -> s.equals("http") || s.equals("https"))
.filter(s -> s.equals(HTTP) || s.equals(HTTPS))
.findFirst()
.orElse("http");
.orElse(HTTP);

setRequestUrl(String.format("%s://%s%s", schemeToUse, OasModelHelper.getHost(openApiDoc), OasModelHelper.getBasePath(openApiDoc)));
}
Expand Down Expand Up @@ -190,4 +199,14 @@ public boolean isValidateOptionalFields() {
public void setValidateOptionalFields(boolean validateOptionalFields) {
this.validateOptionalFields = validateOptionalFields;
}

public String getRootContextPath() {
return rootContextPath;
}

public void setRootContextPath(String rootContextPath) {
this.rootContextPath = rootContextPath;
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.citrusframework.openapi;

import java.util.Map;
import org.citrusframework.spi.ResourcePathTypeResolver;
import org.citrusframework.spi.TypeResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Interface for processing OpenAPI specifications.
* <p>
* This interface is designed to be implemented by custom processors that handle OpenAPI specifications.
* Implementations of this interface are discovered by the standard citrus SPI mechanism.
* </p>
*/
public interface OpenApiSpecificationProcessor {

/** Logger */
Logger logger = LoggerFactory.getLogger(OpenApiSpecificationProcessor.class);

/** Message validator resource lookup path */
String RESOURCE_PATH = "META-INF/citrus/openapi/processor";

/** Type resolver to find custom message validators on classpath via resource path lookup */
TypeResolver TYPE_RESOLVER = new ResourcePathTypeResolver(RESOURCE_PATH);

void process(OpenApiSpecification openApiSpecification);

/**
* Resolves all available validators from resource path lookup. Scans classpath for validator meta information
* and instantiates those validators.
*/
static Map<String, OpenApiSpecificationProcessor> lookup() {
Map<String, OpenApiSpecificationProcessor> validators = TYPE_RESOLVER.resolveAll("", TypeResolver.DEFAULT_TYPE_PROPERTY, "name");

if (logger.isDebugEnabled()) {
validators.forEach((k, v) -> logger.debug(String.format("Found openapi specification processor '%s' as %s", k, v.getClass())));
}

return validators;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@

package org.citrusframework.openapi;

import io.apicurio.datamodels.openapi.models.OasSchema;
import java.util.Map;
import java.util.stream.Collectors;

import io.apicurio.datamodels.openapi.models.OasSchema;
import org.citrusframework.CitrusSettings;
import org.citrusframework.context.TestContext;
import org.citrusframework.exceptions.CitrusRuntimeException;
import org.citrusframework.openapi.model.OasModelHelper;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
Expand Down Expand Up @@ -349,4 +349,52 @@ public static String createRandomValueExpression(OasSchema schema) {
return "";
}
}

/**
* Create validation expression using regex according to schema type and format.
* @param name
* @param oasSchema
* @return
*/
public static String createValidationRegex(String name, OasSchema oasSchema) {

if (oasSchema != null && (OasModelHelper.isReferenceType(oasSchema) || OasModelHelper.isObjectType(oasSchema))) {
throw new CitrusRuntimeException(String.format("Unable to create a validation regex for an reference of object schema '%s'!", name));
}

return createValidationRegex(oasSchema);
}

public static String createValidationRegex(OasSchema schema) {

if (schema == null) {
return "";
}

switch (schema.type) {
case "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]\\d";
} else if (StringUtils.hasText(schema.pattern)) {
return schema.pattern;
} else if (!CollectionUtils.isEmpty(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 "number":
return "[0-9]+\\.?[0-9]*";
case "integer":
return "[0-9]+";
case "boolean":
return "(true|false)";
default:
return "";
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public OpenApiClientRequestActionBuilder send(String operationId) {
* Uses default Http status 200 OK.
*/
public OpenApiClientResponseActionBuilder receive(String operationId) {
return receive(operationId, HttpStatus.OK);
return receive(operationId, (String)null);
}

/**
Expand Down Expand Up @@ -136,11 +136,11 @@ public TestActionBuilder<?> getDelegate() {
*/
@Override
public void setReferenceResolver(ReferenceResolver referenceResolver) {
if (referenceResolver == null) {
if (referenceResolver != null) {
this.referenceResolver = referenceResolver;

if (delegate instanceof ReferenceResolverAware) {
((ReferenceResolverAware) delegate).setReferenceResolver(referenceResolver);
if (delegate instanceof ReferenceResolverAware referenceResolverAware) {
referenceResolverAware.setReferenceResolver(referenceResolver);
}
}
}
Expand Down
Loading

0 comments on commit 1b1adfe

Please sign in to comment.