package com.qdesrame.openapi.diff.output; import static com.qdesrame.openapi.diff.model.Changed.result; import static j2html.TagCreator.*; import com.qdesrame.openapi.diff.model.*; import com.qdesrame.openapi.diff.utils.RefPointer; import com.qdesrame.openapi.diff.utils.RefType; import io.swagger.v3.oas.models.media.ArraySchema; import io.swagger.v3.oas.models.media.MediaType; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.responses.ApiResponse; import j2html.tags.ContainerTag; import java.util.List; import java.util.Map; import java.util.Optional; public class HtmlRender implements Render { private String title; private String linkCss; protected static RefPointer<Schema> refPointer = new RefPointer<>(RefType.SCHEMAS); protected ChangedOpenApi diff; public HtmlRender() { this("Api Change Log", "http://deepoove.com/swagger-diff/stylesheets/demo.css"); } public HtmlRender(String title, String linkCss) { this.title = title; this.linkCss = linkCss; } public String render(ChangedOpenApi diff) { this.diff = diff; List<Endpoint> newEndpoints = diff.getNewEndpoints(); ContainerTag ol_newEndpoint = ol_newEndpoint(newEndpoints); List<Endpoint> missingEndpoints = diff.getMissingEndpoints(); ContainerTag ol_missingEndpoint = ol_missingEndpoint(missingEndpoints); List<Endpoint> deprecatedEndpoints = diff.getDeprecatedEndpoints(); ContainerTag ol_deprecatedEndpoint = ol_deprecatedEndpoint(deprecatedEndpoints); List<ChangedOperation> changedOperations = diff.getChangedOperations(); ContainerTag ol_changed = ol_changed(changedOperations); return renderHtml(ol_newEndpoint, ol_missingEndpoint, ol_deprecatedEndpoint, ol_changed); } public String renderHtml( ContainerTag ol_new, ContainerTag ol_miss, ContainerTag ol_deprec, ContainerTag ol_changed) { ContainerTag html = html() .attr("lang", "en") .with( head() .with( meta().withCharset("utf-8"), title(title), link().withRel("stylesheet").withHref(linkCss)), body() .with( header().with(h1(title)), div() .withClass("article") .with( div().with(h2("What's New"), hr(), ol_new), div().with(h2("What's Deleted"), hr(), ol_miss), div().with(h2("What's Deprecated"), hr(), ol_deprec), div().with(h2("What's Changed"), hr(), ol_changed)))); return document().render() + html.render(); } private ContainerTag ol_newEndpoint(List<Endpoint> endpoints) { if (null == endpoints) return ol(); ContainerTag ol = ol(); for (Endpoint endpoint : endpoints) { ol.with( li_newEndpoint( endpoint.getMethod().toString(), endpoint.getPathUrl(), endpoint.getSummary())); } return ol; } private ContainerTag li_newEndpoint(String method, String path, String desc) { return li().with(span(method).withClass(method)).withText(path + " ").with(span(desc)); } private ContainerTag ol_missingEndpoint(List<Endpoint> endpoints) { if (null == endpoints) return ol(); ContainerTag ol = ol(); for (Endpoint endpoint : endpoints) { ol.with( li_missingEndpoint( endpoint.getMethod().toString(), endpoint.getPathUrl(), endpoint.getSummary())); } return ol; } private ContainerTag li_missingEndpoint(String method, String path, String desc) { return li().with(span(method).withClass(method), del().withText(path)).with(span(" " + desc)); } private ContainerTag ol_deprecatedEndpoint(List<Endpoint> endpoints) { if (null == endpoints) return ol(); ContainerTag ol = ol(); for (Endpoint endpoint : endpoints) { ol.with( li_deprecatedEndpoint( endpoint.getMethod().toString(), endpoint.getPathUrl(), endpoint.getSummary())); } return ol; } private ContainerTag li_deprecatedEndpoint(String method, String path, String desc) { return li().with(span(method).withClass(method), del().withText(path)).with(span(" " + desc)); } private ContainerTag ol_changed(List<ChangedOperation> changedOperations) { if (null == changedOperations) return ol(); ContainerTag ol = ol(); for (ChangedOperation changedOperation : changedOperations) { String pathUrl = changedOperation.getPathUrl(); String method = changedOperation.getHttpMethod().toString(); String desc = Optional.ofNullable(changedOperation.getSummary()) .map(ChangedMetadata::getRight) .orElse(""); ContainerTag ul_detail = ul().withClass("detail"); if (result(changedOperation.getParameters()).isDifferent()) { ul_detail.with( li().with(h3("Parameters")).with(ul_param(changedOperation.getParameters()))); } if (changedOperation.resultRequestBody().isDifferent()) { ul_detail.with( li().with(h3("Request")) .with(ul_request(changedOperation.getRequestBody().getContent()))); } else { } if (changedOperation.resultApiResponses().isDifferent()) { ul_detail.with( li().with(h3("Response")).with(ul_response(changedOperation.getApiResponses()))); } ol.with( li().with(span(method).withClass(method)) .withText(pathUrl + " ") .with(span(desc)) .with(ul_detail)); } return ol; } private ContainerTag ul_response(ChangedApiResponse changedApiResponse) { Map<String, ApiResponse> addResponses = changedApiResponse.getIncreased(); Map<String, ApiResponse> delResponses = changedApiResponse.getMissing(); Map<String, ChangedResponse> changedResponses = changedApiResponse.getChanged(); ContainerTag ul = ul().withClass("change response"); for (String propName : addResponses.keySet()) { ul.with(li_addResponse(propName, addResponses.get(propName))); } for (String propName : delResponses.keySet()) { ul.with(li_missingResponse(propName, delResponses.get(propName))); } for (String propName : changedResponses.keySet()) { ul.with(li_changedResponse(propName, changedResponses.get(propName))); } return ul; } private ContainerTag li_addResponse(String name, ApiResponse response) { return li().withText(String.format("New response : [%s]", name)) .with( span(null == response.getDescription() ? "" : ("//" + response.getDescription())) .withClass("comment")); } private ContainerTag li_missingResponse(String name, ApiResponse response) { return li().withText(String.format("Deleted response : [%s]", name)) .with( span(null == response.getDescription() ? "" : ("//" + response.getDescription())) .withClass("comment")); } private ContainerTag li_changedResponse(String name, ChangedResponse response) { return li().withText(String.format("Changed response : [%s]", name)) .with( span((null == response.getNewApiResponse() || null == response.getNewApiResponse().getDescription()) ? "" : ("//" + response.getNewApiResponse().getDescription())) .withClass("comment")) .with(ul_request(response.getContent())); } private ContainerTag ul_request(ChangedContent changedContent) { ContainerTag ul = ul().withClass("change request-body"); if (changedContent != null) { for (String propName : changedContent.getIncreased().keySet()) { ul.with(li_addRequest(propName, changedContent.getIncreased().get(propName))); } for (String propName : changedContent.getMissing().keySet()) { ul.with(li_missingRequest(propName, changedContent.getMissing().get(propName))); } for (String propName : changedContent.getChanged().keySet()) { ul.with(li_changedRequest(propName, changedContent.getChanged().get(propName))); } } return ul; } private ContainerTag li_addRequest(String name, MediaType request) { return li().withText(String.format("New body: '%s'", name)); } private ContainerTag li_missingRequest(String name, MediaType request) { return li().withText(String.format("Deleted body: '%s'", name)); } private ContainerTag li_changedRequest(String name, ChangedMediaType request) { ContainerTag li = li().with(div_changedSchema(request.getSchema())) .withText(String.format("Changed body: '%s'", name)); if (request.isIncompatible()) { incompatibilities(li, request.getSchema()); } return li; } private ContainerTag div_changedSchema(ChangedSchema schema) { ContainerTag div = div(); div.with(h3("Schema" + (schema.isIncompatible() ? " incompatible" : ""))); return div; } private void incompatibilities(final ContainerTag output, final ChangedSchema schema) { incompatibilities(output, "", schema); } private void incompatibilities( final ContainerTag output, String propName, final ChangedSchema schema) { if (schema.getItems() != null) { items(output, propName, schema.getItems()); } if (schema.isCoreChanged() == DiffResult.INCOMPATIBLE && schema.isChangedType()) { String type = type(schema.getOldSchema()) + " -> " + type(schema.getNewSchema()); property(output, propName, "Changed property type", type); } String prefix = propName.isEmpty() ? "" : propName + "."; properties( output, prefix, "Missing property", schema.getMissingProperties(), schema.getContext()); schema .getChangedProperties() .forEach((name, property) -> incompatibilities(output, prefix + name, property)); } private void items(ContainerTag output, String propName, ChangedSchema schema) { incompatibilities(output, propName + "[n]", schema); } private void properties( ContainerTag output, String propPrefix, String title, Map<String, Schema> properties, DiffContext context) { if (properties != null) { properties.forEach((key, value) -> property(output, propPrefix + key, title, resolve(value))); } } protected void property(ContainerTag output, String name, String title, Schema schema) { property(output, name, title, type(schema)); } protected void property(ContainerTag output, String name, String title, String type) { output.with(p(String.format("%s: %s (%s)", title, name, type)).withClass("missing")); } protected Schema resolve(Schema schema) { return refPointer.resolveRef( diff.getNewSpecOpenApi().getComponents(), schema, schema.get$ref()); } protected String type(Schema schema) { String result = "object"; if (schema instanceof ArraySchema) { result = "array"; } else if (schema.getType() != null) { result = schema.getType(); } return result; } private ContainerTag ul_param(ChangedParameters changedParameters) { List<Parameter> addParameters = changedParameters.getIncreased(); List<Parameter> delParameters = changedParameters.getMissing(); List<ChangedParameter> changed = changedParameters.getChanged(); ContainerTag ul = ul().withClass("change param"); for (Parameter param : addParameters) { ul.with(li_addParam(param)); } for (ChangedParameter param : changed) { ul.with(li_changedParam(param)); } for (Parameter param : delParameters) { ul.with(li_missingParam(param)); } return ul; } private ContainerTag li_addParam(Parameter param) { return li().withText("Add " + param.getName() + " in " + param.getIn()) .with( span(null == param.getDescription() ? "" : ("//" + param.getDescription())) .withClass("comment")); } private ContainerTag li_missingParam(Parameter param) { return li().withClass("missing") .with(span("Delete")) .with(del(param.getName())) .with(span("in ").withText(param.getIn())) .with( span(null == param.getDescription() ? "" : ("//" + param.getDescription())) .withClass("comment")); } private ContainerTag li_deprecatedParam(ChangedParameter param) { return li().withClass("missing") .with(span("Deprecated")) .with(del(param.getName())) .with(span("in ").withText(param.getIn())) .with( span(null == param.getNewParameter().getDescription() ? "" : ("//" + param.getNewParameter().getDescription())) .withClass("comment")); } private ContainerTag li_changedParam(ChangedParameter changeParam) { if (changeParam.isDeprecated()) { return li_deprecatedParam(changeParam); } boolean changeRequired = changeParam.isChangeRequired(); boolean changeDescription = changeParam.getDescription().isDifferent(); Parameter rightParam = changeParam.getNewParameter(); Parameter leftParam = changeParam.getNewParameter(); ContainerTag li = li().withText(changeParam.getName() + " in " + changeParam.getIn()); if (changeRequired) { li.withText(" change into " + (rightParam.getRequired() ? "required" : "not required")); } if (changeDescription) { li.withText(" Notes ") .with(del(leftParam.getDescription()).withClass("comment")) .withText(" change into ") .with(span(rightParam.getDescription()).withClass("comment")); } return li; } }