Fix issue that OpenAPI specification had errors when validating it against Swagger/OpenAPI schema and Swagger/OpenAPI spec (#5465)

* Fix issue that OpenAPI specification had errors when validating it against Swagger/OpenAPI schema and Swagger/OpenAPI spec

* Add changelog

* Consolidate swagger version

---------

Co-authored-by: Primož Delopst <primoz.delopst@better.care>
Co-authored-by: James Agnew <jamesagnew@gmail.com>
This commit is contained in:
Primož Delopst 2024-01-09 19:07:54 +01:00 committed by GitHub
parent 55c3066789
commit adfdbab0f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 93 additions and 11 deletions

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 5465
title: "Several fixes to the HAPI FHIR generated OpenAPI schema have been implemented. This means that
the spec now validates cleanly. Thanks to Primož Delopst for the contribution!"

View File

@ -35,6 +35,7 @@ import ca.uhn.fhir.util.ClasspathUtil;
import ca.uhn.fhir.util.ExtensionConstants;
import ca.uhn.fhir.util.HapiExtensions;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.collect.ImmutableList;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import io.swagger.v3.core.util.Yaml;
@ -47,9 +48,13 @@ import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.DateSchema;
import io.swagger.v3.oas.models.media.DateTimeSchema;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.NumberSchema;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.responses.ApiResponse;
@ -73,6 +78,7 @@ import org.hl7.fhir.r4.model.CapabilityStatement;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.OperationDefinition;
@ -109,6 +115,7 @@ import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.function.Supplier;
@ -679,9 +686,10 @@ public class OpenApiInterceptor {
operation.addParametersItem(parametersItem);
parametersItem.setName(nextSearchParam.getName());
parametersItem.setRequired(false);
parametersItem.setIn("query");
parametersItem.setDescription(nextSearchParam.getDocumentation());
parametersItem.setStyle(Parameter.StyleEnum.SIMPLE);
parametersItem.setSchema(toSchema(nextSearchParam.getType()));
}
}
@ -765,7 +773,13 @@ public class OpenApiInterceptor {
"/" + theResourceType + "/$" + operationDefinition.getCode(),
PathItem.HttpMethod.GET);
populateOperation(
theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, true);
theFhirContext,
theOpenApi,
theResourceType,
operationDefinition,
operation,
"/" + theResourceType + "/$" + operationDefinition.getCode(),
PathItem.HttpMethod.GET);
}
if (operationDefinition.getInstance()) {
Operation operation = getPathItem(
@ -774,13 +788,26 @@ public class OpenApiInterceptor {
PathItem.HttpMethod.GET);
addResourceIdParameter(operation);
populateOperation(
theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, true);
theFhirContext,
theOpenApi,
theResourceType,
operationDefinition,
operation,
"/" + theResourceType + "/{id}/$" + operationDefinition.getCode(),
PathItem.HttpMethod.GET);
}
} else {
if (operationDefinition.getSystem()) {
Operation operation =
getPathItem(thePaths, "/$" + operationDefinition.getCode(), PathItem.HttpMethod.GET);
populateOperation(theFhirContext, theOpenApi, null, operationDefinition, operation, true);
populateOperation(
theFhirContext,
theOpenApi,
null,
operationDefinition,
operation,
"/$" + operationDefinition.getCode(),
PathItem.HttpMethod.GET);
}
}
}
@ -793,7 +820,13 @@ public class OpenApiInterceptor {
"/" + theResourceType + "/$" + operationDefinition.getCode(),
PathItem.HttpMethod.POST);
populateOperation(
theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, false);
theFhirContext,
theOpenApi,
theResourceType,
operationDefinition,
operation,
"/" + theResourceType + "/$" + operationDefinition.getCode(),
PathItem.HttpMethod.POST);
}
if (operationDefinition.getInstance()) {
Operation operation = getPathItem(
@ -802,13 +835,26 @@ public class OpenApiInterceptor {
PathItem.HttpMethod.POST);
addResourceIdParameter(operation);
populateOperation(
theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, false);
theFhirContext,
theOpenApi,
theResourceType,
operationDefinition,
operation,
"/" + theResourceType + "/{id}/$" + operationDefinition.getCode(),
PathItem.HttpMethod.POST);
}
} else {
if (operationDefinition.getSystem()) {
Operation operation =
getPathItem(thePaths, "/$" + operationDefinition.getCode(), PathItem.HttpMethod.POST);
populateOperation(theFhirContext, theOpenApi, null, operationDefinition, operation, false);
populateOperation(
theFhirContext,
theOpenApi,
null,
operationDefinition,
operation,
"/$" + operationDefinition.getCode(),
PathItem.HttpMethod.POST);
}
}
}
@ -847,16 +893,18 @@ public class OpenApiInterceptor {
String theResourceType,
OperationDefinition theOperationDefinition,
Operation theOperation,
boolean theGet) {
String thePath,
PathItem.HttpMethod httpMethod) {
if (theResourceType == null) {
theOperation.addTagsItem(PAGE_SYSTEM);
} else {
theOperation.addTagsItem(theResourceType);
}
theOperation.setSummary(theOperationDefinition.getTitle());
theOperation.setSummary(Optional.ofNullable(theOperationDefinition.getTitle())
.orElse(String.format("%s: %s", httpMethod.name(), thePath)));
theOperation.setDescription(theOperationDefinition.getDescription());
addFhirResourceResponse(theFhirContext, theOpenApi, theOperation, null);
if (theGet) {
if (httpMethod == PathItem.HttpMethod.GET) {
for (OperationDefinition.OperationDefinitionParameterComponent nextParameter :
theOperationDefinition.getParameter()) {
@ -873,8 +921,8 @@ public class OpenApiInterceptor {
parametersItem.setName(nextParameter.getName());
parametersItem.setIn("query");
parametersItem.setDescription(nextParameter.getDocumentation());
parametersItem.setStyle(Parameter.StyleEnum.SIMPLE);
parametersItem.setRequired(nextParameter.getMin() > 0);
parametersItem.setSchema(toSchema(nextParameter.getSearchType()));
List<Extension> exampleExtensions =
nextParameter.getExtensionsByUrl(HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE);
@ -1024,6 +1072,7 @@ public class OpenApiInterceptor {
parameter.setIn("path");
parameter.setDescription("The resource version ID");
parameter.setExample("1");
parameter.setRequired(true);
parameter.setSchema(new Schema().type("string").minimum(new BigDecimal(1)));
parameter.setStyle(Parameter.StyleEnum.SIMPLE);
theOperation.addParametersItem(parameter);
@ -1085,6 +1134,7 @@ public class OpenApiInterceptor {
parameter.setIn("path");
parameter.setDescription("The resource ID");
parameter.setExample("123");
parameter.setRequired(true);
parameter.setSchema(new Schema().type("string").minimum(new BigDecimal(1)));
parameter.setStyle(Parameter.StyleEnum.SIMPLE);
theOperation.addParametersItem(parameter);
@ -1188,4 +1238,31 @@ public class OpenApiInterceptor {
return builder.toString();
}
}
private Schema<?> toSchema(Enumerations.SearchParamType type) {
if (type == null) {
return new StringSchema();
}
switch (type) {
case NUMBER:
return new NumberSchema();
case DATE:
Schema<?> dateSchema = new Schema<>();
dateSchema.anyOf(ImmutableList.of(new DateTimeSchema(), new DateSchema()));
return dateSchema;
case QUANTITY:
Schema<?> quantitySchema = new Schema<>();
quantitySchema.anyOf(ImmutableList.of(new StringSchema(), new NumberSchema()));
return quantitySchema;
case STRING:
case TOKEN:
case REFERENCE:
case COMPOSITE:
case URI:
case SPECIAL:
case NULL:
default:
return new StringSchema();
}
}
}