Skip to content

Commit

Permalink
fix: add draft open api documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Thorsten Schlathoelter authored and bbortt committed Jan 27, 2025
1 parent 1d7c2e1 commit 955c5fc
Show file tree
Hide file tree
Showing 50 changed files with 1,013 additions and 412 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@

package org.citrusframework.openapi;

import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.synchronizedList;
import static org.citrusframework.openapi.OpenApiSettings.isNeglectBasePathGlobally;
import static org.citrusframework.openapi.OpenApiSettings.isRequestValidationEnabledGlobally;
import static org.citrusframework.openapi.OpenApiSettings.isResponseValidationEnabledGlobally;

import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotNull;
Expand All @@ -28,6 +32,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.citrusframework.exceptions.CitrusRuntimeException;
import org.citrusframework.openapi.validation.OpenApiValidationPolicy;
import org.citrusframework.repository.BaseRepository;
import org.citrusframework.spi.Resource;
Expand Down Expand Up @@ -57,11 +62,22 @@ public class OpenApiRepository extends BaseRepository {
*/
private String rootContextPath;

private boolean requestValidationEnabled = true;
/**
* Flag to indicate whether the base path of the OpenAPI should be part of the path or not.
*/
private boolean neglectBasePath = isNeglectBasePathGlobally();

/**
* Flag to indicate whether OpenAPIs managed by this repository should perform request validation.
*/
private boolean requestValidationEnabled = isRequestValidationEnabledGlobally();

private boolean responseValidationEnabled = true;
/**
* Flag to indicate whether OpenAPIs managed by this repository should perform response validation.
*/
private boolean responseValidationEnabled = isResponseValidationEnabledGlobally();

private OpenApiValidationPolicy openApiValidationPolicy = OpenApiSettings.getOpenApiValidationPolicy();
private OpenApiValidationPolicy validationPolicy = OpenApiSettings.getOpenApiValidationPolicy();

public OpenApiRepository() {
super(DEFAULT_NAME);
Expand Down Expand Up @@ -124,6 +140,24 @@ public void setRootContextPath(String rootContextPath) {
this.rootContextPath = rootContextPath;
}

public OpenApiRepository rootContextPath(String rootContextPath) {
setRootContextPath(rootContextPath);
return this;
}

public boolean isNeglectBasePath() {
return neglectBasePath;
}

public void setNeglectBasePath(boolean neglectBasePath) {
this.neglectBasePath = neglectBasePath;
}

public OpenApiRepository neglectBasePath(boolean neglectBasePath) {
setNeglectBasePath(neglectBasePath);
return this;
}

public boolean isRequestValidationEnabled() {
return requestValidationEnabled;
}
Expand All @@ -132,6 +166,11 @@ public void setRequestValidationEnabled(boolean requestValidationEnabled) {
this.requestValidationEnabled = requestValidationEnabled;
}

public OpenApiRepository requestValidationEnabled(boolean requestValidationEnabled) {
setRequestValidationEnabled(requestValidationEnabled);
return this;
}

public boolean isResponseValidationEnabled() {
return responseValidationEnabled;
}
Expand All @@ -140,15 +179,24 @@ public void setResponseValidationEnabled(boolean responseValidationEnabled) {
this.responseValidationEnabled = responseValidationEnabled;
}

public OpenApiValidationPolicy getOpenApiValidationPolicy() {
return openApiValidationPolicy;
public OpenApiRepository responseValidationEnabled(boolean responseValidationEnabled) {
setResponseValidationEnabled(responseValidationEnabled);
return this;
}

public void setOpenApiValidationPolicy(
OpenApiValidationPolicy openApiValidationPolicy) {
this.openApiValidationPolicy = openApiValidationPolicy;
public OpenApiValidationPolicy getValidationPolicy() {
return validationPolicy;
}

public void setValidationPolicy(
OpenApiValidationPolicy validationPolicy) {
this.validationPolicy = validationPolicy;
}

public OpenApiRepository validationPolicy(OpenApiValidationPolicy openApiValidationPolicy) {
setValidationPolicy(openApiValidationPolicy);
return this;
}

/**
* Adds an OpenAPI Specification specified by the given resource to the repository. If an alias
Expand All @@ -158,14 +206,21 @@ public void setOpenApiValidationPolicy(
*/
@Override
public void addRepository(Resource openApiResource) {
OpenApiSpecification openApiSpecification = OpenApiSpecification.from(openApiResource,
openApiValidationPolicy);
determineResourceAlias(openApiResource).ifPresent(openApiSpecification::addAlias);
openApiSpecification.setApiRequestValidationEnabled(requestValidationEnabled);
openApiSpecification.setApiResponseValidationEnabled(responseValidationEnabled);
openApiSpecification.setRootContextPath(rootContextPath);

addRepository(openApiSpecification);

try {
OpenApiSpecification openApiSpecification = OpenApiSpecification.from(openApiResource,
validationPolicy);
determineResourceAlias(openApiResource).ifPresent(openApiSpecification::addAlias);
openApiSpecification.setApiRequestValidationEnabled(requestValidationEnabled);
openApiSpecification.setApiResponseValidationEnabled(responseValidationEnabled);
openApiSpecification.setRootContextPath(rootContextPath);
openApiSpecification.neglectBasePath(neglectBasePath);
addRepository(openApiSpecification);
} catch (Exception e) {
logger.error(format("Unable to read OpenApiSpecification from location: %s", openApiResource.getURI()));
throw new CitrusRuntimeException(e);
}

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,10 @@ public class OpenApiSpecification {
private static final String HTTP = "http";

/**
* An uid to uniquely identify this specification at runtime. The uid is based on the SHA of the
* OpenAPI document and the root context, to which it is attached.
* A unique identifier (UID) for this specification at runtime. The UID is generated based on the SHA
* of the OpenAPI document combined with the full context path to which the API is attached.
*
* @see OpenApiSpecification#determineUid for detailed information on how the UID is generated.
*/
private String uid;

Expand Down Expand Up @@ -221,18 +223,16 @@ public static OpenApiSpecification from(URL specUrl,
OpenApiValidationContext openApiValidationContext;
if (specUrl.getProtocol().startsWith(HTTPS)) {
openApiDoc = OpenApiResourceLoader.fromSecuredWebResource(specUrl);
openApiValidationContext = OpenApiValidationContextLoader.fromSecuredWebResource(
specUrl);
} else {
openApiDoc = OpenApiResourceLoader.fromWebResource(specUrl);
openApiValidationContext = OpenApiValidationContextLoader.fromWebResource(specUrl,
openApiValidationPolicy);
}

openApiValidationContext = OpenApiValidationContextLoader.fromSpec(OasModelHelper.toJson(openApiDoc), openApiValidationPolicy);
specification.setOpenApiValidationContext(openApiValidationContext);

specification.setSpecUrl(specUrl.toString());
specification.initPathLookups();
specification.setOpenApiDoc(openApiDoc);
specification.setOpenApiValidationContext(openApiValidationContext);
specification.setRequestUrl(
format("%s://%s%s%s", specUrl.getProtocol(), specUrl.getHost(),
specUrl.getPort() > 0 ? ":" + specUrl.getPort() : "",
Expand Down Expand Up @@ -267,7 +267,7 @@ public static OpenApiSpecification from(Resource resource,
OasDocument openApiDoc = OpenApiResourceLoader.fromFile(resource);

specification.setOpenApiValidationContext(
OpenApiValidationContextLoader.fromFile(resource, openApiValidationPolicy));
OpenApiValidationContextLoader.fromSpec(OasModelHelper.toJson(openApiDoc), openApiValidationPolicy));
specification.setOpenApiDoc(openApiDoc);

String schemeToUse = Optional.ofNullable(OasModelHelper.getSchemes(openApiDoc))
Expand All @@ -280,7 +280,7 @@ public static OpenApiSpecification from(Resource resource,
specification.setSpecUrl(resource.getLocation());
specification.setRequestUrl(
format("%s://%s%s", schemeToUse, OasModelHelper.getHost(openApiDoc),
specification.rootContextPath));
getBasePath(openApiDoc)));

return specification;
}
Expand Down Expand Up @@ -314,10 +314,21 @@ public static OpenApiSpecification fromString(String openApi) {
return specification;
}

/**
* Get the UID of this specification.
*/
public String getUid() {
return uid;
}

/**
* Get the unique id of the given operation.
*/
@SuppressWarnings("unused")
public String getUniqueId(OasOperation oasOperation) {
return operationToUniqueId.get(oasOperation);
}

public synchronized OasDocument getOpenApiDoc(TestContext context) {
if (openApiDoc != null) {
return openApiDoc;
Expand Down Expand Up @@ -351,13 +362,8 @@ public synchronized OasDocument getOpenApiDoc(TestContext context) {
URL specWebResource = toSpecUrl(resolvedSpecUrl);
if (resolvedSpecUrl.startsWith(HTTPS)) {
initApiDoc(() -> OpenApiResourceLoader.fromSecuredWebResource(specWebResource));
setOpenApiValidationContext(
OpenApiValidationContextLoader.fromSecuredWebResource(specWebResource));
} else {
initApiDoc(() -> OpenApiResourceLoader.fromWebResource(specWebResource));
setOpenApiValidationContext(
OpenApiValidationContextLoader.fromWebResource(specWebResource,
openApiValidationPolicy));
}

if (requestUrl == null) {
Expand All @@ -370,8 +376,7 @@ public synchronized OasDocument getOpenApiDoc(TestContext context) {
} else {
Resource resource = Resources.create(resolvedSpecUrl);
initApiDoc(() -> OpenApiResourceLoader.fromFile(resource));
setOpenApiValidationContext(OpenApiValidationContextLoader.fromFile(resource,
openApiValidationPolicy));


if (requestUrl == null) {
String schemeToUse = Optional.ofNullable(OasModelHelper.getSchemes(openApiDoc))
Expand All @@ -388,6 +393,10 @@ public synchronized OasDocument getOpenApiDoc(TestContext context) {
}
}

setOpenApiValidationContext(
OpenApiValidationContextLoader.fromSpec(OasModelHelper.toJson(openApiDoc),
openApiValidationPolicy));

return openApiDoc;
}

Expand Down Expand Up @@ -427,8 +436,7 @@ private void initPathLookups() {
return;
}

uid = DigestUtils.sha256Hex(OasModelHelper.toJson(openApiDoc) + rootContextPath);
aliases.add(uid);
determineUid();

operationIdToOperationPathAdapter.clear();
OasModelHelper.visitOasOperations(this.openApiDoc, (oasPathItem, oasOperation) -> {
Expand All @@ -446,6 +454,14 @@ private void initPathLookups() {
});
}

private void determineUid() {
if (uid != null) {
aliases.remove(uid);
}
uid = DigestUtils.sha256Hex(OasModelHelper.toJson(openApiDoc) + getFullContextPath());
aliases.add(uid);
}

/**
* Stores an {@link OperationPathAdapter} in
* {@link org.citrusframework.openapi.OpenApiSpecification#operationIdToOperationPathAdapter}.
Expand All @@ -470,6 +486,7 @@ private void storeOperationPathAdapter(OasOperation operation, OasPathItem pathI
appendSegmentToUrlPath(fullContextPath, path), operation, uniqueOperationId);

operationIdToOperationPathAdapter.put(uniqueOperationId, operationPathAdapter);

if (hasText(operation.operationId)) {
operationIdToOperationPathAdapter.put(operation.operationId, operationPathAdapter);
}
Expand Down Expand Up @@ -556,8 +573,9 @@ public void setRootContextPath(String rootContextPath) {
initPathLookups();
}

public OpenApiValidationPolicy getOpenApiValidationPolicy() {
return openApiValidationPolicy;
public OpenApiSpecification rootContextPath(String rootContextPath) {
setRootContextPath(rootContextPath);
return this;
}

public void addAlias(String alias) {
Expand Down Expand Up @@ -608,15 +626,6 @@ public void initOpenApiDoc(TestContext context) {
}
}

public OpenApiSpecification withRootContext(String rootContextPath) {
setRootContextPath(rootContextPath);
return this;
}

public String getUniqueId(OasOperation oasOperation) {
return operationToUniqueId.get(oasOperation);
}

/**
* Get the full path for the given {@link OasPathItem}.
* <p>
Expand All @@ -633,12 +642,31 @@ public String getUniqueId(OasOperation oasOperation) {
* item
*/
public String getFullPath(OasPathItem oasPathItem) {
return appendSegmentToUrlPath(rootContextPath,
getFullBasePath(oasPathItem));
}

/**
* Get the full base-path for the given {@link OasPathItem}.
* <p>
* The full base-path is constructed by concatenating the base path (if
* applicable), and the path of the given {@code oasPathItem}. The resulting format is:
* </p>
* <pre>
* /basePath/pathItemPath
* </pre>
* If the base path is to be neglected, it is excluded from the final constructed path.
*
* @param oasPathItem the OpenAPI path item whose full base-path is to be constructed
* @return the full base URL path, consisting of the base path, and the given path
* item
*/
public String getFullBasePath(OasPathItem oasPathItem) {
return appendSegmentToUrlPath(
appendSegmentToUrlPath(rootContextPath,
neglectBasePath ? null : getBasePath(openApiDoc)),
oasPathItem.getPath());
getApplicableBasePath(), oasPathItem.getPath());
}


/**
* Constructs the full context path for the given {@link OasPathItem}.
* <p>
Expand All @@ -654,7 +682,7 @@ public String getFullPath(OasPathItem oasPathItem) {
*/
public String getFullContextPath() {
return appendSegmentToUrlPath(rootContextPath,
neglectBasePath ? null : getBasePath(openApiDoc));
getApplicableBasePath());
}

/**
Expand All @@ -674,9 +702,23 @@ public void setNeglectBasePath(boolean neglectBasePath) {
}

public OpenApiSpecification neglectBasePath(boolean neglectBasePath) {
this.neglectBasePath = neglectBasePath;
initPathLookups();
setNeglectBasePath(neglectBasePath);
return this;
}

/**
* Gets the base path if basePath should be applied.
*/
private String getApplicableBasePath() {
return neglectBasePath ? "" : getBasePath(openApiDoc);
}


/**
* Add another alias for this specification.
*/
public OpenApiSpecification alias(String alias) {
addAlias(alias);
return this;
}
}
Loading

0 comments on commit 955c5fc

Please sign in to comment.