#3466 fix and improve openapi generation (#4436)

* Update fhir version constants from 4.0.0 to 4.0.1

Yes, this means the test fails.  The root cause of the failure is possibly the incorrect fhir version
in org.hl7.fhir.r4/src/main/java/org/hl7/fhir/r4/model/Constants.java

* fixes hapifhir/hapi-fhir#3466

Includes a POST version for every GET endpoint.
Excludes non-primitive parameters from GET endpoints.
For an idempotent (affectsState=false) endpoint with at least one required non-primitive parameter, only support POST

Also include */_search endpoints for the Search operation.
This commit is contained in:
Michael Lawley 2023-01-18 04:31:54 +10:00 committed by GitHub
parent 23b593aa17
commit ee06f900fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 93 additions and 38 deletions

View File

@ -77,6 +77,9 @@ import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.Type; import org.hl7.fhir.r4.model.Type;
import org.hl7.fhir.r4.model.OperationDefinition.OperationDefinitionParameterComponent;
import org.hl7.fhir.r4.model.OperationDefinition.OperationParameterUse;
import org.hl7.fhir.r4.model.codesystems.DataTypes;
import org.thymeleaf.IEngineConfiguration; import org.thymeleaf.IEngineConfiguration;
import org.thymeleaf.TemplateEngine; import org.thymeleaf.TemplateEngine;
import org.thymeleaf.cache.AlwaysValidCacheEntryValidity; import org.thymeleaf.cache.AlwaysValidCacheEntryValidity;
@ -98,6 +101,7 @@ import java.io.InputStream;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -569,21 +573,8 @@ public class OpenApiInterceptor {
// Search // Search
if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.SEARCHTYPE)) { if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.SEARCHTYPE)) {
Operation operation = getPathItem(paths, "/" + resourceType, PathItem.HttpMethod.GET); addSearchOperation(openApi, getPathItem(paths, "/" + resourceType, PathItem.HttpMethod.GET), ctx, resourceType, nextResource);
operation.addTagsItem(resourceType); addSearchOperation(openApi, getPathItem(paths, "/" + resourceType + "/_search", PathItem.HttpMethod.GET), ctx, resourceType, nextResource);
operation.setDescription("This is a search type");
operation.setSummary("search-type: Search for " + resourceType + " instances");
addFhirResourceResponse(ctx, openApi, operation, null);
for (CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent nextSearchParam : nextResource.getSearchParam()) {
Parameter parametersItem = new Parameter();
operation.addParametersItem(parametersItem);
parametersItem.setName(nextSearchParam.getName());
parametersItem.setIn("query");
parametersItem.setDescription(nextSearchParam.getDocumentation());
parametersItem.setStyle(Parameter.StyleEnum.SIMPLE);
}
} }
// Resource-level Operations // Resource-level Operations
@ -596,6 +587,24 @@ public class OpenApiInterceptor {
return openApi; return openApi;
} }
protected void addSearchOperation(final OpenAPI openApi, final Operation operation, final FhirContext ctx,
final String resourceType, final CapabilityStatement.CapabilityStatementRestResourceComponent nextResource) {
operation.addTagsItem(resourceType);
operation.setDescription("This is a search type");
operation.setSummary("search-type: Search for " + resourceType + " instances");
addFhirResourceResponse(ctx, openApi, operation, null);
for (final CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent nextSearchParam : nextResource.getSearchParam()) {
final Parameter parametersItem = new Parameter();
operation.addParametersItem(parametersItem);
parametersItem.setName(nextSearchParam.getName());
parametersItem.setIn("query");
parametersItem.setDescription(nextSearchParam.getDocumentation());
parametersItem.setStyle(Parameter.StyleEnum.SIMPLE);
}
}
private Supplier<IBaseResource> patchExampleSupplier() { private Supplier<IBaseResource> patchExampleSupplier() {
return () -> { return () -> {
Parameters example = new Parameters(); Parameters example = new Parameters();
@ -650,8 +659,15 @@ public class OpenApiInterceptor {
} }
OperationDefinition operationDefinition = toCanonicalVersion(operationDefinitionNonCanonical); OperationDefinition operationDefinition = toCanonicalVersion(operationDefinitionNonCanonical);
final boolean postOnly = operationDefinition.getAffectsState()
|| operationDefinition.getParameter().stream()
.filter(p -> p.getUse().equals(OperationParameterUse.IN))
.anyMatch(p -> {
final boolean required = p.getMin() > 0;
return required && !isPrimitive(p);
});
if (!operationDefinition.getAffectsState()) { if (!postOnly) {
// GET form for non-state-affecting operations // GET form for non-state-affecting operations
if (theResourceType != null) { if (theResourceType != null) {
@ -671,30 +687,57 @@ public class OpenApiInterceptor {
} }
} }
} else {
// POST form for all operations
if (theResourceType != null) {
if (operationDefinition.getType()) {
Operation operation = getPathItem(thePaths, "/" + theResourceType + "/$" + operationDefinition.getCode(), PathItem.HttpMethod.POST);
populateOperation(theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, false);
}
if (operationDefinition.getInstance()) {
Operation operation = getPathItem(thePaths, "/" + theResourceType + "/{id}/$" + operationDefinition.getCode(), PathItem.HttpMethod.POST);
addResourceIdParameter(operation);
populateOperation(theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, false);
}
} else {
if (operationDefinition.getSystem()) {
Operation operation = getPathItem(thePaths, "/$" + operationDefinition.getCode(), PathItem.HttpMethod.POST);
populateOperation(theFhirContext, theOpenApi, null, operationDefinition, operation, false);
}
}
} }
// POST form for all operations
if (theResourceType != null) {
if (operationDefinition.getType()) {
Operation operation = getPathItem(thePaths, "/" + theResourceType + "/$" + operationDefinition.getCode(), PathItem.HttpMethod.POST);
populateOperation(theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, false);
}
if (operationDefinition.getInstance()) {
Operation operation = getPathItem(thePaths, "/" + theResourceType + "/{id}/$" + operationDefinition.getCode(), PathItem.HttpMethod.POST);
addResourceIdParameter(operation);
populateOperation(theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, false);
}
} else {
if (operationDefinition.getSystem()) {
Operation operation = getPathItem(thePaths, "/$" + operationDefinition.getCode(), PathItem.HttpMethod.POST);
populateOperation(theFhirContext, theOpenApi, null, operationDefinition, operation, false);
}
}
} }
} }
private static List<String> primitiveTypes = Arrays.asList(
DataTypes.BOOLEAN.toCode(),
DataTypes.INTEGER.toCode(),
DataTypes.STRING.toCode(),
DataTypes.DECIMAL.toCode(),
DataTypes.URI.toCode(),
DataTypes.URL.toCode(),
DataTypes.CANONICAL.toCode(),
DataTypes.REFERENCE.toCode(),
DataTypes.BASE64BINARY.toCode(),
DataTypes.INSTANT.toCode(),
DataTypes.DATE.toCode(),
DataTypes.DATETIME.toCode(),
DataTypes.TIME.toCode(),
DataTypes.CODE.toCode(),
DataTypes.CODING.toCode(),
DataTypes.OID.toCode(),
DataTypes.ID.toCode(),
DataTypes.MARKDOWN.toCode(),
DataTypes.UNSIGNEDINT.toCode(),
DataTypes.POSITIVEINT.toCode(),
DataTypes.UUID.toCode()
);
private static boolean isPrimitive(OperationDefinitionParameterComponent parameter) {
return primitiveTypes.contains(parameter.getType());
}
private void populateOperation(FhirContext theFhirContext, OpenAPI theOpenApi, String theResourceType, OperationDefinition theOperationDefinition, Operation theOperation, boolean theGet) { private void populateOperation(FhirContext theFhirContext, OpenAPI theOpenApi, String theResourceType, OperationDefinition theOperationDefinition, Operation theOperation, boolean theGet) {
if (theResourceType == null) { if (theResourceType == null) {
theOperation.addTagsItem(PAGE_SYSTEM); theOperation.addTagsItem(PAGE_SYSTEM);
@ -704,10 +747,15 @@ public class OpenApiInterceptor {
theOperation.setSummary(theOperationDefinition.getTitle()); theOperation.setSummary(theOperationDefinition.getTitle());
theOperation.setDescription(theOperationDefinition.getDescription()); theOperation.setDescription(theOperationDefinition.getDescription());
addFhirResourceResponse(theFhirContext, theOpenApi, theOperation, null); addFhirResourceResponse(theFhirContext, theOpenApi, theOperation, null);
if (theGet) { if (theGet) {
for (OperationDefinition.OperationDefinitionParameterComponent nextParameter : theOperationDefinition.getParameter()) { for (OperationDefinition.OperationDefinitionParameterComponent nextParameter : theOperationDefinition.getParameter()) {
if ("0".equals(nextParameter.getMax()) || !nextParameter.getUse().equals(OperationParameterUse.IN)) {
continue;
}
if (!isPrimitive(nextParameter) && nextParameter.getMin() == 0) {
continue;
}
Parameter parametersItem = new Parameter(); Parameter parametersItem = new Parameter();
theOperation.addParametersItem(parametersItem); theOperation.addParametersItem(parametersItem);
@ -733,6 +781,9 @@ public class OpenApiInterceptor {
Parameters exampleRequestBody = new Parameters(); Parameters exampleRequestBody = new Parameters();
for (OperationDefinition.OperationDefinitionParameterComponent nextSearchParam : theOperationDefinition.getParameter()) { for (OperationDefinition.OperationDefinitionParameterComponent nextSearchParam : theOperationDefinition.getParameter()) {
if ("0".equals(nextSearchParam.getMax()) || !nextSearchParam.getUse().equals(OperationParameterUse.IN)) {
continue;
}
Parameters.ParametersParameterComponent param = exampleRequestBody.addParameter(); Parameters.ParametersParameterComponent param = exampleRequestBody.addParameter();
param.setName(nextSearchParam.getName()); param.setName(nextSearchParam.getName());
String paramType = nextSearchParam.getType(); String paramType = nextSearchParam.getType();

View File

@ -122,7 +122,11 @@ public class OpenApiInterceptorTest {
assertEquals("Foo Op Short", fooOpPath.getPost().getSummary()); assertEquals("Foo Op Short", fooOpPath.getPost().getSummary());
PathItem lastNPath = parsed.getPaths().get("/Observation/$lastn"); PathItem lastNPath = parsed.getPaths().get("/Observation/$lastn");
assertNull(lastNPath.getPost()); assertNotNull(lastNPath.getPost());
assertEquals("LastN Description", lastNPath.getPost().getDescription());
assertEquals("LastN Short", lastNPath.getPost().getSummary());
assertNull(lastNPath.getPost().getParameters());
assertNotNull(lastNPath.getPost().getRequestBody());
assertNotNull(lastNPath.getGet()); assertNotNull(lastNPath.getGet());
assertEquals("LastN Description", lastNPath.getGet().getDescription()); assertEquals("LastN Description", lastNPath.getGet().getDescription());
assertEquals("LastN Short", lastNPath.getGet().getSummary()); assertEquals("LastN Short", lastNPath.getGet().getSummary());