diff --git a/src/main/java/com/gentics/vertx/openapi/OpenAPIv3Generator.java b/src/main/java/com/gentics/vertx/openapi/OpenAPIv3Generator.java index f317792..e1b129a 100644 --- a/src/main/java/com/gentics/vertx/openapi/OpenAPIv3Generator.java +++ b/src/main/java/com/gentics/vertx/openapi/OpenAPIv3Generator.java @@ -98,8 +98,8 @@ public class OpenAPIv3Generator { * @param maybePathBlacklist optional regex for API path blacklist * @param maybePathWhitelist optional regex for API path whitelist */ - public OpenAPIv3Generator(String version, List servers, - @Nonnull Optional> maybePathBlacklist, + public OpenAPIv3Generator(String version, List servers, + @Nonnull Optional> maybePathBlacklist, @Nonnull Optional> maybePathWhitelist) { this(version, servers, null, maybePathBlacklist, maybePathWhitelist); } @@ -111,9 +111,9 @@ public OpenAPIv3Generator(String version, List servers, * @param maybePathBlacklist optional regex for API path blacklist * @param maybePathWhitelist optional regex for API path whitelist */ - public OpenAPIv3Generator(String version, List servers, + public OpenAPIv3Generator(String version, List servers, @Nonnull Map security, - @Nonnull Optional> maybePathBlacklist, + @Nonnull Optional> maybePathBlacklist, @Nonnull Optional> maybePathWhitelist) { this.maybePathBlacklist = maybePathBlacklist; this.maybePathWhitelist = maybePathWhitelist; @@ -130,9 +130,9 @@ public OpenAPIv3Generator(String version, List servers, * @param pretty prettify the output * @param maybePathItemTransformer an optional custom path and path item transformer * @return - * @throws OpenAPIGenerationException + * @throws OpenAPIGenerationException */ - public String generate(Map routers, Format format, boolean pretty, + public String generate(Map routers, Format format, boolean pretty, @Nonnull Optional> maybePathItemTransformer, @Nonnull Optional>>> maybeExtraComponentSupplier) throws OpenAPIGenerationException { return generate("Created with Gentics Vert.x OpenAPI generator", routers, format, pretty, false, maybePathItemTransformer, maybeExtraComponentSupplier); @@ -147,7 +147,7 @@ public String generate(Map routers, Format format, boolean prett * @param useVersion31 switch between OpenAPI spec versions v3.1 and v3.0 * @param maybePathItemTransformer an optional custon path and path item transformer * @return the generated spec text - * @throws OpenAPIGenerationException + * @throws OpenAPIGenerationException */ public String generate(String name, Map routers, Format format, boolean pretty, boolean useVersion31, @Nonnull Optional> maybePathItemTransformer, @@ -304,50 +304,50 @@ protected void fillComponent(Class cls, OpenAPI openApi) { log.debug(" - Generics: " + Arrays.toString(generics.toArray())); } Map properties = fieldStreams.stream().flatMap(Function.identity()) - .filter(f -> !Modifier.isStatic(f.getModifiers())).peek(f -> { - Class t = f.getType(); + .filter(f -> !Modifier.isStatic(f.getModifiers())).peek(f -> { + Class t = f.getType(); if (!t.isPrimitive() && !t.getCanonicalName().startsWith("java.lang") && !t.getCanonicalName().startsWith("java.lang")) { - fillComponent(t, openApi); - } - }) - .map(f -> { - String name = f.getName(); - log.debug(" - Field: " + f); - Schema fieldSchema = new Schema(); - fieldSchema.setName(name); - - JsonProperty property = f.getAnnotation(JsonProperty.class); - if (property != null) { - if (StringUtils.isNotBlank(property.defaultValue())) { - fieldSchema.setDefault(property.defaultValue()); - } - if (StringUtils.isNotBlank(property.value())) { - name = property.value(); + fillComponent(t, openApi); } - if (property.required()) { - schema.addRequiredItem(name); + }) + .map(f -> { + String name = f.getName(); + log.debug(" - Field: " + f); + Schema fieldSchema = new Schema(); + fieldSchema.setName(name); + + JsonProperty property = f.getAnnotation(JsonProperty.class); + if (property != null) { + if (StringUtils.isNotBlank(property.defaultValue())) { + fieldSchema.setDefault(property.defaultValue()); + } + if (StringUtils.isNotBlank(property.value())) { + name = property.value(); + } + if (property.required()) { + schema.addRequiredItem(name); + } } - } - Class t = f.getType(); - JsonDeserialize jdes = f.getAnnotation(JsonDeserialize.class); - if (jdes != null && jdes.as() != null) { - t = jdes.as(); - fieldSchema.setType("object"); - fieldSchema.set$ref("#/components/schemas/" + getComponentName(t)); - } else { + Class t = f.getType(); + JsonDeserialize jdes = f.getAnnotation(JsonDeserialize.class); + if (jdes != null && jdes.as() != null) { + t = jdes.as(); + fieldSchema.setType("object"); + fieldSchema.set$ref("#/components/schemas/" + getComponentName(t)); + } else { generics.addAll(Arrays.asList(ParameterizedType.class.isInstance(f.getGenericType()) ? ParameterizedType.class.cast(f.getGenericType()).getActualTypeArguments() : new Type[0])); - if (generics.size() > 0) { - log.debug(" - Generics: " + Arrays.toString(generics.toArray())); + if (generics.size() > 0) { + log.debug(" - Generics: " + Arrays.toString(generics.toArray())); + } + fillType(t, fieldSchema, generics, openApi); } - fillType(t, fieldSchema, generics, openApi); - } - Boolean filledAndRequired = fillComponentFromAnnotation(f, fieldSchema); - if (filledAndRequired != null && filledAndRequired) { - schema.addRequiredItem(name); - } - fieldSchema.setTypes(Collections.singleton(fieldSchema.getType())); - return new UnmodifiableMapEntry<>(name, fieldSchema); - }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + Boolean filledAndRequired = fillComponentFromAnnotation(f, fieldSchema); + if (filledAndRequired != null && filledAndRequired) { + schema.addRequiredItem(name); + } + fieldSchema.setTypes(Collections.singleton(fieldSchema.getType())); + return new UnmodifiableMapEntry<>(name, fieldSchema); + }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); schema.setProperties(properties); } @@ -364,6 +364,14 @@ protected void resolveEndpointRoute(String path, PathItem pathItem, InternalEndp log.debug("Path {} is marked as hidden and skipped", path); return; } + + if (StringUtils.isNotBlank(endpoint.getDisplayName())) { + operation.setSummary(endpoint.getDisplayName()); + } + if (StringUtils.isNotBlank(endpoint.getDescription())) { + operation.setDescription(endpoint.getDescription()); + } + HttpMethod method = endpoint.getMethod(); if (method == null) { method = HttpMethod.GET; @@ -400,51 +408,61 @@ protected void resolveEndpointRoute(String path, PathItem pathItem, InternalEndp return new UnmodifiableMapEntry("default", response); }).filter(Objects::nonNull).forEach(e -> responses.addApiResponse(e.getKey(), e.getValue())); endpoint.getExampleResponses().entrySet().stream().filter(e -> Objects.nonNull(e.getValue())) - .map(e -> { - ApiResponse response = new ApiResponse(); - if (e.getValue().getDescription().startsWith("Generated login token")) { - e.getValue().getHeaders(); - } - response.setDescription(e.getValue().getDescription()); - Content responseBody = new Content(); - if (endpoint.getExampleResponseClasses() != null && endpoint.getExampleResponseClasses().get(e.getKey()) != null) { - Class ref = endpoint.getExampleResponseClasses().get(e.getKey()); - Schema schema = new Schema<>(); - schema.set$ref("#/components/schemas/" + ref.getSimpleName()); - MediaType mediaType = new MediaType(); - mediaType.setSchema(schema); + .map(e -> { + ApiResponse response = new ApiResponse(); + if (e.getValue().getDescription().startsWith("Generated login token")) { + e.getValue().getHeaders(); + } + response.setDescription(e.getValue().getDescription()); + Content responseBody = new Content(); + + Class ref = null; + if (endpoint.getExampleResponseClasses() != null) { + ref = endpoint.getExampleResponseClasses().get(e.getKey()); + } + String mimeKey = null; + org.raml.model.MimeType bodyMime = null; if (e.getValue() != null && e.getValue().getBody() != null) { - org.raml.model.MimeType bodyMime = e.getValue().getBody().get("application/json"); + bodyMime = e.getValue().getBody().get("application/json"); + mimeKey = "application/json"; if (bodyMime == null) { bodyMime = e.getValue().getBody().get("text/plain"); + mimeKey = "text/plain"; } - if (bodyMime != null) { - String exampleText = bodyMime.getExample(); - if (exampleText != null) { - exampleText = exampleText.trim(); - try { - if (exampleText.startsWith("{")) { - mediaType.setExample(new io.vertx.core.json.JsonObject(exampleText).getMap()); - } else if (exampleText.startsWith("[")) { - mediaType.setExample(new io.vertx.core.json.JsonArray(exampleText).getList()); - } else { - mediaType.setExample(exampleText); - } - } catch (Exception ex) { + } + MediaType mediaType = new MediaType(); + if (ref != null) { + Schema schema = new Schema<>(); + schema.set$ref("#/components/schemas/" + getComponentName(ref)); + mediaType.setSchema(schema); + fillComponent(ref, openApi); + } + if (bodyMime != null) { + String exampleText = bodyMime.getExample(); + if (exampleText != null) { + exampleText = exampleText.trim(); + try { + if (exampleText.startsWith("{")) { + mediaType.setExample(new io.vertx.core.json.JsonObject(exampleText).getMap()); + } else if (exampleText.startsWith("[")) { + mediaType.setExample(new io.vertx.core.json.JsonArray(exampleText).getList()); + } else { mediaType.setExample(exampleText); } + } catch (Exception ex) { + mediaType.setExample(exampleText); } - } else { - mediaType.setExample(e.getValue()); } - } else { - mediaType.setExample(e.getValue()); } - responseBody.addMediaType("application/json", mediaType); - response.setContent(responseBody); - fillComponent(ref, openApi); - } - return new UnmodifiableMapEntry(e.getKey(), response); + if (mimeKey == null) { + mimeKey = ref != null ? "application/json" : null; + } + if (mimeKey != null) { + responseBody.addMediaType(mimeKey, mediaType); + response.setContent(responseBody); + } + + return new UnmodifiableMapEntry(e.getKey(), response); }).filter(Objects::nonNull).forEach(e -> responses.addApiResponse(Integer.toString(e.getKey()), e.getValue())); operation.setResponses(responses); if (endpoint.getExampleRequestMap() != null && !HttpMethod.DELETE.equals(method)) { @@ -468,32 +486,32 @@ protected void resolveEndpointRoute(String path, PathItem pathItem, InternalEndp */ protected void resolveMethod(String methodName, PathItem pathItem, Operation operation) { switch (methodName.toUpperCase()) { - case "DELETE": - pathItem.setDelete(operation); - break; - case "GET": - pathItem.setGet(operation); - break; - case "HEAD": - pathItem.setHead(operation); - break; - case "OPTIONS": - pathItem.setOptions(operation); - break; - case "PATCH": - pathItem.setPatch(operation); - break; - case "POST": - pathItem.setPost(operation); - break; - case "PUT": - pathItem.setPut(operation); - break; - case "TRACE": - pathItem.setTrace(operation); - break; - default: - break; + case "DELETE": + pathItem.setDelete(operation); + break; + case "GET": + pathItem.setGet(operation); + break; + case "HEAD": + pathItem.setHead(operation); + break; + case "OPTIONS": + pathItem.setOptions(operation); + break; + case "PATCH": + pathItem.setPatch(operation); + break; + case "POST": + pathItem.setPost(operation); + break; + case "PUT": + pathItem.setPut(operation); + break; + case "TRACE": + pathItem.setTrace(operation); + break; + default: + break; } } @@ -534,8 +552,8 @@ protected void addRoute(String parent, Route route, OpenAPI consumer, Optional segment.startsWith(":") ? ("{" + segment.substring(1) + "}") : segment) - .collect(Collectors.joining("/")))) + .map(segment -> segment.startsWith(":") ? ("{" + segment.substring(1) + "}") : segment) + .collect(Collectors.joining("/")))) .replace("//", "/"); if(maybePathBlacklist.flatMap(list -> list.stream().filter(blacklisted -> blacklisted.matcher(path).matches()).findAny()).isPresent() @@ -553,16 +571,14 @@ protected void addRoute(String parent, Route route, OpenAPI consumer, Optional { - log.debug("Path with metadata: " + path); - pathItem.setSummary(endpoint.getDisplayName()); - pathItem.setDescription(endpoint.getDescription()); - endpoint.getModel().forEach(modelComponent -> fillComponent(modelComponent, consumer)); - resolveEndpointRoute(path, pathItem, endpoint, consumer); - }, () -> { - resolveFallbackRoute(route, pathItem); - }); + .map(InternalEndpointRoute.class::cast) + .ifPresentOrElse(endpoint -> { + log.debug("Path with metadata: " + path); + endpoint.getModel().forEach(modelComponent -> fillComponent(modelComponent, consumer)); + resolveEndpointRoute(path, pathItem, endpoint, consumer); + }, () -> { + resolveFallbackRoute(route, pathItem); + }); String path1 = maybePathItemTransformer.map(pathItemTransformer -> { String newPath = pathItemTransformer.apply(path, pathItem); if (!Strings.CI.equals(path, newPath)) { @@ -617,7 +633,7 @@ protected void resolveFallbackRoute(Route route, PathItem pathItem) { * @param key * @param mimeType * @param refClass - * @param openApi + * @param openApi * @return */ @SuppressWarnings("rawtypes") @@ -647,9 +663,9 @@ protected Map.Entry fillMediaType(String key, MimeType mimeTy schema.setType("object"); mediaType.setSchema(schema); return new UnmodifiableMapEntry(key, mediaType); - } else { + } else { return new UnmodifiableMapEntry(key, mediaType); - } + } } /** @@ -704,7 +720,7 @@ private void fillType(Class modelClass, Schema fieldSchema, List generi } else { log.error("Unknown generic array type: {} / {}", modelClass, Arrays.toString(generics.toArray())); } - }); + }); } fieldSchema.setItems(itemSchema); } else { @@ -776,38 +792,38 @@ private void fillType(Class modelClass, Schema fieldSchema, List generi protected final Parameter parameter(String name, AbstractParam param, InParameter inType) { Schema schema; switch (param.getType()) { - case BOOLEAN: - schema = new Schema(); - schema.setType("boolean"); - break; - case DATE: - schema = new Schema(); - schema.setType("integer"); - schema.setFormat("int64"); - break; - case FILE: - schema = new Schema(); - schema.setType("string"); - schema.setFormat("binary"); - break; - case INTEGER: - schema = new Schema(); - schema.setType("integer"); - schema.setFormat("int32"); - break; - case NUMBER: - schema = new Schema(); - schema.setType("number"); - schema.setFormat("double"); - break; - case STRING: - schema = new Schema(); - schema.setType("string"); - break; - default: - schema = new Schema(); - schema.setType("object"); - break; + case BOOLEAN: + schema = new Schema(); + schema.setType("boolean"); + break; + case DATE: + schema = new Schema(); + schema.setType("integer"); + schema.setFormat("int64"); + break; + case FILE: + schema = new Schema(); + schema.setType("string"); + schema.setFormat("binary"); + break; + case INTEGER: + schema = new Schema(); + schema.setType("integer"); + schema.setFormat("int32"); + break; + case NUMBER: + schema = new Schema(); + schema.setType("number"); + schema.setFormat("double"); + break; + case STRING: + schema = new Schema(); + schema.setType("string"); + break; + default: + schema = new Schema(); + schema.setType("object"); + break; } schema.setMinimum(param.getMinimum()); schema.setMaximum(param.getMaximum()); diff --git a/src/main/java/com/gentics/vertx/openapi/metadata/InternalEndpointRouteImpl.java b/src/main/java/com/gentics/vertx/openapi/metadata/InternalEndpointRouteImpl.java index 2523485..1ba3817 100644 --- a/src/main/java/com/gentics/vertx/openapi/metadata/InternalEndpointRouteImpl.java +++ b/src/main/java/com/gentics/vertx/openapi/metadata/InternalEndpointRouteImpl.java @@ -11,6 +11,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -84,8 +85,8 @@ public class InternalEndpointRouteImpl implements InternalEndpointRoute { /** * Map of example responses for the corresponding status code. */ - protected final Map exampleResponses = new HashMap<>(); - protected final Map> exampleResponseClasses = new HashMap<>(); + protected final Map exampleResponses = new LinkedHashMap<>(); + protected final Map> exampleResponseClasses = new LinkedHashMap<>(); protected final Set consumes = new LinkedHashSet<>(); protected final Set produces = new LinkedHashSet<>(); protected final Map parameters = new HashMap<>(); @@ -372,8 +373,24 @@ public InternalEndpointRoute exampleResponse(HttpResponseStatus status, Object m mimeType.setSchema(getSchema(model.getClass())); map.put("application/json", mimeType); } else { - mimeType.setExample(model.toString()); - if (model.getClass().getSimpleName().toLowerCase().startsWith("json")) { + String exampleText = null; + try { + if (model instanceof JsonObject) { + exampleText = ((JsonObject) model).encode(); + } else if (model instanceof JsonArray) { + exampleText = ((JsonArray) model).encode(); + } else { + exampleText = defaultMapper.writeValueAsString(model); + } + } catch (Exception e) { + exampleText = model.toString(); + } + + mimeType.setExample(exampleText); + mimeType.setSchema(getSchema(model.getClass())); + if (exampleText != null && (exampleText.startsWith("{") || exampleText.startsWith("["))) { + map.put("application/json", mimeType); + } else if (model.getClass().getSimpleName().toLowerCase().startsWith("json")) { map.put("application/json", mimeType); } else { map.put("text/plain", mimeType); @@ -421,7 +438,13 @@ public InternalEndpointRoute exampleRequest(RestModel model) { String json = model.toJson(false); mimeType.setExample(json); mimeType.setSchema(getSchema(model.getClass())); - bodyMap.put("application/json", mimeType); + + if (consumes != null && consumes.contains("multipart/form-data")) { + bodyMap.put("multipart/form-data", mimeType); + } else { + bodyMap.put("application/json", mimeType); + } + this.exampleRequestMap = bodyMap; this.exampleRequestClass = model.getClass(); return this; diff --git a/src/main/java/com/gentics/vertx/openapi/route/InternalEndpointBuilder.java b/src/main/java/com/gentics/vertx/openapi/route/InternalEndpointBuilder.java index b370919..ee195fd 100644 --- a/src/main/java/com/gentics/vertx/openapi/route/InternalEndpointBuilder.java +++ b/src/main/java/com/gentics/vertx/openapi/route/InternalEndpointBuilder.java @@ -38,9 +38,9 @@ public final class InternalEndpointBuilder { private String consumes; private Router subRouter; private Boolean useNormalisedPath; - private Pair exampleResponse; - private Triple exampleResponseModel; - private Object[] exampleResponseHeader; + private List> exampleResponses; + private List> exampleResponseModels; + private List exampleResponseHeaders; private String produces; private String pathRegex; private String displayName; @@ -177,7 +177,10 @@ public InternalEndpointBuilder useNormalisedPath(boolean useNormalisedPath) { * @return Fluent API */ public InternalEndpointBuilder withExampleResponse(HttpResponseStatus status, String description) { - this.exampleResponse = Pair.of(status, description); + if (this.exampleResponses == null) { + this.exampleResponses = new ArrayList<>(1); + } + this.exampleResponses.add(Pair.of(status, description)); return this; } @@ -193,7 +196,10 @@ public InternalEndpointBuilder withExampleResponse(HttpResponseStatus status, St * @return Fluent API */ public InternalEndpointBuilder withExampleResponse(HttpResponseStatus status, Object model, String description) { - this.exampleResponseModel = Triple.of(status, model, description); + if (this.exampleResponseModels == null) { + this.exampleResponseModels = new ArrayList<>(1); + } + this.exampleResponseModels.add(Triple.of(status, model, description)); return this; } @@ -213,7 +219,10 @@ public InternalEndpointBuilder withExampleResponse(HttpResponseStatus status, Ob * @return */ public InternalEndpointBuilder withExampleResponse(HttpResponseStatus status, String description, String headerName, String example, String headerDescription) { - this.exampleResponseHeader = new Object[] {status, description, headerName, example, headerDescription }; + if (this.exampleResponseHeaders == null) { + this.exampleResponseHeaders = new ArrayList<>(1); + } + this.exampleResponseHeaders.add(new Object[] { status, description, headerName, example, headerDescription }); return this; } @@ -495,14 +504,14 @@ public InternalEndpointRoute build() { if (useNormalisedPath != null) { endpoint.useNormalisedPath(useNormalisedPath); } - if (exampleResponse != null) { - endpoint.exampleResponse(exampleResponse.getKey(), exampleResponse.getValue()); + if (exampleResponses != null) { + exampleResponses.forEach(er -> endpoint.exampleResponse(er.getKey(), er.getValue())); } - if (exampleResponseModel != null) { - endpoint.exampleResponse(exampleResponseModel.getLeft(), exampleResponseModel.getMiddle(), exampleResponseModel.getRight()); + if (exampleResponseModels != null) { + exampleResponseModels.forEach(erm -> endpoint.exampleResponse(erm.getLeft(), erm.getMiddle(), erm.getRight())); } - if (exampleResponseHeader != null) { - endpoint.exampleResponse((HttpResponseStatus) exampleResponseHeader[0], (String) exampleResponseHeader[1], (String) exampleResponseHeader[2], (String) exampleResponseHeader[3], (String) exampleResponseHeader[4]); + if (exampleResponseHeaders != null) { + exampleResponseHeaders.forEach(erh -> endpoint.exampleResponse((HttpResponseStatus) erh[0], (String) erh[1], (String) erh[2], (String) erh[3], (String) erh[4])); } if (produces != null) { endpoint.produces(produces);