Add additional resource docs to generated OpenAPI documentation (#5359)

* Add additional resource docs to generated OpenAPI documentation

* Test fix
This commit is contained in:
James Agnew 2023-10-10 20:36:33 -04:00 committed by GitHub
parent 424f26b897
commit 88e9780004
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 128 additions and 29 deletions

View File

@ -0,0 +1,8 @@
---
type: add
issue: 5355
title: "The generated OpenAPI documentation produced by OpenApiInterceptor will now include
additional details in the individual resource type documentation, including the values of
*CapabilityStatement.rest.resource.documentation*,
*CapabilityStatement.rest.resource.profile*, and
*CapabilityStatement.rest.resource.supportedProfile*."

View File

@ -64,6 +64,7 @@ import org.hl7.fhir.convertors.factory.VersionConvertorFactory_43_50;
import org.hl7.fhir.instance.model.api.IBaseConformance;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.CapabilityStatement;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
@ -108,10 +109,12 @@ import java.util.Properties;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static ca.uhn.fhir.rest.server.util.NarrativeUtil.sanitizeHtmlFragment;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
@ -356,7 +359,7 @@ public class OpenApiInterceptor {
String copyright = cs.getCopyright();
if (isNotBlank(copyright)) {
copyright = myFlexmarkRenderer.render(myFlexmarkParser.parse(copyright));
copyright = renderMarkdown(copyright);
context.setVariable("COPYRIGHT_HTML", copyright);
}
@ -411,6 +414,11 @@ public class OpenApiInterceptor {
theResponse.getWriter().close();
}
@Nonnull
private String renderMarkdown(String copyright) {
return myFlexmarkRenderer.render(myFlexmarkParser.parse(copyright));
}
protected void populateOIDCVariables(ServletRequestDetails theRequestDetails, WebContext theContext) {
theContext.setVariable("OAUTH2_REDIRECT_URL_PROPERTY", "");
}
@ -515,7 +523,7 @@ public class OpenApiInterceptor {
Tag resourceTag = new Tag();
resourceTag.setName(resourceType);
resourceTag.setDescription("The " + resourceType + " FHIR resource type");
resourceTag.setDescription(createResourceDescription(nextResource));
openApi.addTagsItem(resourceTag);
// Instance Read
@ -624,6 +632,36 @@ public class OpenApiInterceptor {
return openApi;
}
@Nonnull
protected String createResourceDescription(
CapabilityStatement.CapabilityStatementRestResourceComponent theResource) {
StringBuilder b = new StringBuilder();
b.append("The ").append(theResource.getType()).append(" FHIR resource type");
String documentation = theResource.getDocumentation();
if (isNotBlank(documentation)) {
b.append("<br/>");
b.append(sanitizeHtmlFragment(renderMarkdown(documentation)));
}
if (isNotBlank(theResource.getProfile())) {
b.append("<br/>");
b.append("Base profile: ");
b.append(sanitizeHtmlFragment(theResource.getProfile()));
}
for (CanonicalType next : theResource.getSupportedProfile()) {
String nextSupportedProfile = next.getValueAsString();
if (isNotBlank(nextSupportedProfile)) {
b.append("<br/>");
b.append("Supported profile: ");
b.append(sanitizeHtmlFragment(nextSupportedProfile));
}
}
return b.toString();
}
protected void addSearchOperation(
final OpenAPI openApi,
final Operation operation,

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.rest.openapi;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.rest.annotation.*;
@ -32,16 +33,19 @@ import org.apache.http.client.methods.HttpGet;
import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.r5.model.ActorDefinition;
import org.hl7.fhir.r5.model.CapabilityStatement;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Consumer;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
@ -83,6 +87,28 @@ public class OpenApiInterceptorTest {
assertThat(buttonTexts.toString(), buttonTexts, Matchers.contains("All", "System Level Operations", "OperationDefinition 1", "Observation", "Patient"));
}
@Test
public void testResourceDocsCopied() throws IOException {
myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor("OperationDefinition"));
myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor());
myServer.registerInterceptor(new CapabilityStatementEnhancingInterceptor(cs->{
org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceComponent patientResource = findPatientResource(cs);
patientResource.setProfile("http://baseProfile");
patientResource.addSupportedProfile("http://foo");
patientResource.addSupportedProfile("http://bar");
patientResource.setDocumentation("This is **bolded** documentation");
}));
org.hl7.fhir.r4.model.CapabilityStatement cs = myServer.getFhirClient().capabilities().ofType(org.hl7.fhir.r4.model.CapabilityStatement.class).execute();
org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceComponent patientResource = findPatientResource(cs);
assertEquals("This is **bolded** documentation", patientResource.getDocumentation());
String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/";
String resp = fetchSwaggerUi(url);
}
}
@Nested
@ -111,6 +137,35 @@ public class OpenApiInterceptorTest {
}
}
@Interceptor
private static class CapabilityStatementEnhancingInterceptor {
private final Consumer<org.hl7.fhir.r4.model.CapabilityStatement> myConsumer;
public CapabilityStatementEnhancingInterceptor(Consumer<org.hl7.fhir.r4.model.CapabilityStatement> theConsumer) {
myConsumer = theConsumer;
}
@Hook(Pointcut.SERVER_CAPABILITY_STATEMENT_GENERATED)
public void massageCapabilityStatement(IBaseConformance theCs) {
myConsumer.accept((org.hl7.fhir.r4.model.CapabilityStatement) theCs);
}
}
@Nonnull
private static org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceComponent findPatientResource(org.hl7.fhir.r4.model.CapabilityStatement theCs) {
org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceComponent patientResource = theCs
.getRest()
.get(0)
.getResource()
.stream()
.filter(t -> "Patient".equals(t.getType()))
.findFirst()
.orElseThrow();
return patientResource;
}
@SuppressWarnings("JUnitMalformedDeclaration")
abstract static class BaseOpenApiInterceptorTest {

View File

@ -47,10 +47,16 @@ public class NarrativeUtil {
* <li>All other elements and attributes are removed</li>
* </ul>
*/
public static String sanitize(String theHtml) {
XhtmlNode node = new XhtmlNode();
node.setValueAsString(theHtml);
return sanitize(node).getValueAsString();
public static String sanitizeHtmlFragment(String theHtml) {
PolicyFactory idPolicy =
new HtmlPolicyBuilder().allowAttributes("id").globally().toFactory();
PolicyFactory policy = Sanitizers.FORMATTING
.and(Sanitizers.BLOCKS)
.and(Sanitizers.TABLES)
.and(Sanitizers.STYLES)
.and(idPolicy);
return policy.sanitize(theHtml);
}
/**
@ -70,15 +76,7 @@ public class NarrativeUtil {
public static XhtmlNode sanitize(XhtmlNode theNode) {
String html = theNode.getValueAsString();
PolicyFactory idPolicy =
new HtmlPolicyBuilder().allowAttributes("id").globally().toFactory();
PolicyFactory policy = Sanitizers.FORMATTING
.and(Sanitizers.BLOCKS)
.and(Sanitizers.TABLES)
.and(Sanitizers.STYLES)
.and(idPolicy);
String safeHTML = policy.sanitize(html);
String safeHTML = sanitizeHtmlFragment(html);
XhtmlNode retVal = new XhtmlNode();
retVal.setValueAsString(safeHTML);

View File

@ -9,21 +9,21 @@ public class NarrativeUtilTest {
@ParameterizedTest
@CsvSource({
"<div><SPAN ID=\"foo\">hello</SPAN></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\"><span id=\"foo\">hello</span></div>",
"<div><span id=\"foo\">hello</span></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\"><span id=\"foo\">hello</span></div>",
"<div><SPAN ONCLICK=\"hello()\">hello</SPAN></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
"<div><span onclick=\"hello()\">hello</span></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
"<div><a href=\"http://goodbye\">hello</a></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
"<div><table><tr><td>hello</td></tr></table></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\"><table><tbody><tr><td>hello</td></tr></tbody></table></div>",
"<div><span style=\"font-size: 100px;\">hello</span></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\"><span style=\"font-size:100px\">hello</span></div>",
"<div><span style=\"background: url('test.jpg')\">hello</span></div> , <div xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
"hello , <div xmlns=\"http://www.w3.org/1999/xhtml\">hello</div>",
"empty , null",
"null , null"
"<div><SPAN ID=\"foo\">hello</SPAN></div> , <div><span id=\"foo\">hello</span></div>",
"<div><span id=\"foo\">hello</span></div> , <div><span id=\"foo\">hello</span></div>",
"<div><SPAN ONCLICK=\"hello()\">hello</SPAN></div> , <div>hello</div>",
"<div><span onclick=\"hello()\">hello</span></div> , <div>hello</div>",
"<div><a href=\"http://goodbye\">hello</a></div> , <div>hello</div>",
"<div><table><tr><td>hello</td></tr></table></div> , <div><table><tbody><tr><td>hello</td></tr></tbody></table></div>",
"<div><span style=\"font-size: 100px;\">hello</span></div> , <div><span style=\"font-size:100px\">hello</span></div>",
"<div><span style=\"background: url('test.jpg')\">hello</span></div> , <div>hello</div>",
"hello , hello",
"empty , empty",
"null , empty"
})
public void testValidateIsCaseInsensitive(String theHtml, String theExpected) {
String output = NarrativeUtil.sanitize(fixNull(theHtml));
assertEquals(fixNull(theExpected), output);
String output = NarrativeUtil.sanitizeHtmlFragment(fixNull(theHtml));
assertEquals(fixNull(theExpected), fixNull(output));
}
private String fixNull(String theExpected) {

View File

@ -583,7 +583,7 @@ public class BaseController {
theModelMap.put("resultBodyIsLong", resultBodyText.length() > 1000);
theModelMap.put("requestHeaders", requestHeaders);
theModelMap.put("responseHeaders", responseHeaders);
theModelMap.put("narrative", NarrativeUtil.sanitize(narrativeString));
theModelMap.put("narrative", NarrativeUtil.sanitizeHtmlFragment(narrativeString));
theModelMap.put("latencyMs", theLatency);
theModelMap.put("config", myConfig);