io.micronaut.openapi.generator.MicronautInlineModelResolver Maven / Gradle / Ivy
/*
* Copyright 2017-2024 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.openapi.generator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.callbacks.Callback;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.ComposedSchema;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.XML;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.parameters.RequestBody;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.utils.ModelUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
/**
* Inline model resolver.
*
* @since 6.5.0
*/
public final class MicronautInlineModelResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(MicronautInlineModelResolver.class);
// structure mapper sorts properties alphabetically on write to ensure models are
// serialized consistently for lookup of existing models
private static final ObjectMapper STRUCTURE_MAPPER;
static {
STRUCTURE_MAPPER = JsonMapper.builder()
.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true)
.build();
STRUCTURE_MAPPER.writer(new DefaultPrettyPrinter());
}
private OpenAPI openAPI;
private Map addedModels = new HashMap<>();
private Map generatedSignature = new HashMap<>();
private Map inlineSchemaNameMapping = new HashMap<>();
private Map inlineSchemaOptions = new HashMap<>();
private Set inlineSchemaNameMappingValues = new HashSet<>();
private boolean resolveInlineEnums = true;
private boolean skipSchemaReuse = false; // skip reusing inline schema if set to true
private Boolean refactorAllOfInlineSchemas; // refactor allOf inline schemas into $ref
// a set to keep track of names generated for inline schemas
private Set uniqueNames = new HashSet<>();
public MicronautInlineModelResolver(OpenAPI openAPI) {
this.openAPI = openAPI;
this.inlineSchemaOptions.put("ARRAY_ITEM_SUFFIX", "_inner");
this.inlineSchemaOptions.put("MAP_ITEM_SUFFIX", "_value");
}
public void setInlineSchemaNameMapping(Map inlineSchemaNameMapping) {
this.inlineSchemaNameMapping = inlineSchemaNameMapping;
this.inlineSchemaNameMappingValues = new HashSet<>(inlineSchemaNameMapping.values());
}
public void setInlineSchemaOptions(Map inlineSchemaOptions) {
this.inlineSchemaOptions.putAll(inlineSchemaOptions);
if ("true".equalsIgnoreCase(
this.inlineSchemaOptions.getOrDefault("SKIP_SCHEMA_REUSE", "false"))) {
this.skipSchemaReuse = true;
}
if (this.inlineSchemaOptions.containsKey("REFACTOR_ALLOF_INLINE_SCHEMAS")) {
this.refactorAllOfInlineSchemas = Boolean.valueOf(this.inlineSchemaOptions.get("REFACTOR_ALLOF_INLINE_SCHEMAS"));
}
if (this.inlineSchemaOptions.containsKey("RESOLVE_INLINE_ENUMS")) {
this.resolveInlineEnums = Boolean.parseBoolean(this.inlineSchemaOptions.get("RESOLVE_INLINE_ENUMS"));
}
}
private void normalizeArraySchema(Schema schema) {
if (ModelUtils.isArraySchema(schema)) {
schema.setEnum(null);
}
}
public void flatten() {
if (this.openAPI.getComponents() == null) {
this.openAPI.setComponents(new Components());
}
if (this.openAPI.getComponents().getSchemas() == null) {
this.openAPI.getComponents().setSchemas(new HashMap<>());
}
flattenPaths();
flattenComponents();
}
public void flattenPaths() {
Paths paths = openAPI.getPaths();
if (paths == null) {
return;
}
for (Map.Entry pathsEntry : paths.entrySet()) {
PathItem path = pathsEntry.getValue();
if (path.get$ref() != null && path.get$ref().startsWith("#/components/pathItems/")) {
path = openAPI.getComponents().getPathItems().get(path.get$ref().substring("#/components/pathItems/".length()));
}
var operationsMap = new LinkedHashMap<>(path.readOperationsMap());
// use path name (e.g. /foo/bar) and HTTP verb to come up with a name
// in case operationId is not defined later in other methods
String pathname = pathsEntry.getKey();
// Include callback operation as well
for (Map.Entry operationEntry : new LinkedHashMap<>(path.readOperationsMap()).entrySet()) {
Operation operation = operationEntry.getValue();
Map callbacks = operation.getCallbacks();
if (callbacks != null) {
for (Map.Entry callbackEntry : callbacks.entrySet()) {
Callback callback = callbackEntry.getValue();
for (Map.Entry pathItemEntry : callback.entrySet()) {
PathItem pathItem = pathItemEntry.getValue();
operationsMap.putAll(pathItem.readOperationsMap());
}
}
}
}
for (Map.Entry operationEntry : operationsMap.entrySet()) {
Operation operation = operationEntry.getValue();
String inlineSchemaName = getInlineSchemaName(operationEntry.getKey(), pathname);
flattenPathItemParameters(inlineSchemaName, operation, path);
flattenParameters(inlineSchemaName, operation);
flattenRequestBody(inlineSchemaName, operation);
flattenResponses(inlineSchemaName, operation);
}
}
}
private void flattenResponses(String modelName, Operation operation) {
ApiResponses responses = operation.getResponses();
if (responses == null) {
return;
}
for (Map.Entry responsesEntry : responses.entrySet()) {
String key = responsesEntry.getKey();
ApiResponse response = responsesEntry.getValue();
if (response.get$ref() != null && response.get$ref().startsWith("#/components/responses/")) {
response = openAPI.getComponents().getResponses().get(response.get$ref().substring("#/components/responses/".length()));
}
flattenContent(response.getContent(),
(operation.getOperationId() == null ? modelName : operation.getOperationId()) + "_" + key + "_response");
}
}
private void flattenRequestBody(String modelName, Operation operation) {
RequestBody requestBody = operation.getRequestBody();
if (requestBody == null) {
return;
}
// unalias $ref
if (requestBody.get$ref() != null) {
String ref = ModelUtils.getSimpleRef(requestBody.get$ref());
requestBody = openAPI.getComponents().getRequestBodies().get(ref);
if (requestBody == null) {
return;
}
}
flattenContent(requestBody.getContent(),
(operation.getOperationId() == null ? modelName : operation.getOperationId()) + "_request");
}
private void flattenContent(Content content, String name) {
if (content == null || content.isEmpty()) {
return;
}
for (String contentType : content.keySet()) {
MediaType mediaType = content.get(contentType);
if (mediaType == null) {
continue;
}
Schema schema = mediaType.getSchema();
if (schema == null) {
continue;
}
normalizeArraySchema(schema);
String schemaName = resolveModelName(schema.getTitle(), name); // name example: testPost_request
// Recursively gather/make inline models within this schema if any
gatherInlineModels(schema, schemaName);
if (isModelNeeded(schema)) {
// If this schema should be split into its own model, do so
//Schema refSchema = this.makeSchema(schemaName, schema);
mediaType.setSchema(this.makeSchemaInComponents(schemaName, schema));
}
if (schema.getItems() != null && schema.getItems().get$ref() == null) {
String itemsSchemaName = resolveModelName(schema.getTitle(), name); // name example: testPost_request
// Recursively gather/make inline models within this schema if any
gatherInlineModels(schema, itemsSchemaName);
if (isModelNeeded(schema)) {
// If this schema should be split into its own model, do so
//Schema refSchema = this.makeSchema(schemaName, schema);
schema.setItems(this.makeSchemaInComponents(itemsSchemaName, schema));
}
}
}
}
private void flattenParameters(String modelName, Operation operation) {
List parameters = operation.getParameters();
if (parameters == null) {
return;
}
for (Parameter parameter : parameters) {
Schema parameterSchema = parameter.getSchema();
if (parameterSchema == null && parameter.get$ref() == null) {
continue;
}
if (parameter.get$ref() != null && parameter.get$ref().startsWith("#/components/parameters/")) {
if (openAPI.getComponents().getParameters() != null) {
parameter = openAPI.getComponents().getParameters().get(parameter.get$ref().substring("#/components/parameters/".length()));
}
}
parameterSchema = parameter.getSchema();
if (parameterSchema == null) {
continue;
}
normalizeArraySchema(parameterSchema);
String schemaName = resolveModelName(parameterSchema.getTitle(),
(operation.getOperationId() == null ? modelName : operation.getOperationId()) + "_" + parameter.getName() + "_parameter");
// Recursively gather/make inline models within this schema if any
gatherInlineModels(parameterSchema, schemaName);
if (isModelNeeded(parameterSchema)) {
// If this schema should be split into its own model, do so
parameter.setSchema(this.makeSchemaInComponents(schemaName, parameterSchema));
}
var parameterItemsSchema = parameterSchema.getItems();
if (parameterItemsSchema != null && parameterItemsSchema.get$ref() == null) {
String itemsSchemaName = resolveModelName(parameterItemsSchema.getTitle(),
(operation.getOperationId() == null ? modelName : operation.getOperationId()) + "_" + parameter.getName() + "_parameter");
// Recursively gather/make inline models within this schema if any
gatherInlineModels(parameterItemsSchema, itemsSchemaName);
if (isModelNeeded(parameterItemsSchema)) {
// If this schema should be split into its own model, do so
parameterItemsSchema.setItems(this.makeSchemaInComponents(itemsSchemaName, parameterItemsSchema));
}
}
}
}
private void flattenPathItemParameters(String modelName, Operation operation, PathItem pathItem) {
List parameters = new ArrayList<>();
if (pathItem.getParameters() != null) {
parameters.addAll(pathItem.getParameters());
}
if (parameters.isEmpty()) {
return;
}
for (Parameter parameter : parameters) {
if (parameter.getSchema() == null && parameter.get$ref() == null) {
continue;
}
Schema parameterSchema = null;
if (parameter.get$ref() != null && parameter.get$ref().startsWith("#/components/parameters/")) {
if (openAPI.getComponents().getParameters() != null) {
var param = openAPI.getComponents().getParameters().get(parameter.get$ref().substring("#/components/parameters/".length()));
parameterSchema = param.getSchema();
}
} else {
parameterSchema = parameter.getSchema();
}
if (parameterSchema == null) {
continue;
}
normalizeArraySchema(parameterSchema);
String schemaName = resolveModelName(parameterSchema.getTitle(),
(operation.getOperationId() == null ? modelName : operation.getOperationId()) + "_" + parameter.getName() + "_parameter");
// Recursively gather/make inline models within this schema if any
gatherInlineModels(parameterSchema, schemaName);
if (isModelNeeded(parameterSchema)) {
// If this schema should be split into its own model, do so
Schema refSchema = makeSchemaInComponents(schemaName, parameterSchema);
parameter.setSchema(refSchema);
}
var parameterItemsSchema = parameterSchema.getItems();
if (parameterItemsSchema != null && parameterItemsSchema.get$ref() == null) {
String itemsSchemaName = resolveModelName(parameterItemsSchema.getTitle(),
(operation.getOperationId() == null ? modelName : operation.getOperationId()) + "_" + parameter.getName() + "_parameter");
// Recursively gather/make inline models within this schema if any
gatherInlineModels(parameterItemsSchema, itemsSchemaName);
if (isModelNeeded(parameterItemsSchema)) {
// If this schema should be split into its own model, do so
parameterSchema.setItems(this.makeSchemaInComponents(itemsSchemaName, parameterItemsSchema));
}
}
}
}
/**
* Recursively gather inline models that need to be generated and
* replace inline schemas with $ref to schema to-be-generated.
*
* @param schema target schema
* @param modelPrefix model name (usually the prefix of the inline model name)
*/
private void gatherInlineModels(Schema schema, String modelPrefix) {
if (schema.get$ref() != null) {
// if ref already, no inline schemas should be present but check for
// any to catch OpenAPI violations
if (isModelNeeded(schema) || "object".equals(schema.getType()) ||
schema.getProperties() != null || schema.getAdditionalProperties() != null ||
ModelUtils.isComposedSchema(schema)) {
LOGGER.error("Illegal schema found with $ref combined with other properties, no properties should be defined alongside a $ref:\n {}", schema);
}
return;
}
// Check object models / any type models / composed models for properties,
// if the schema has a type defined that is not "object" it should not define
// any properties
if (schema.getType() == null || "object".equals(schema.getType())) {
// Check properties and recurse, each property could be its own inline model
Map props = schema.getProperties();
if (props != null) {
for (String propName : props.keySet()) {
Schema prop = props.get(propName);
if (prop == null) {
continue;
}
normalizeArraySchema(prop);
String schemaName = resolveModelName(prop.getTitle(), modelPrefix + "_" + propName);
// Recurse to create $refs for inner models
gatherInlineModels(prop, schemaName);
if (isModelNeeded(prop)) {
// If this schema should be split into its own model, do so
Schema refSchema = makeSchemaInComponents(schemaName, prop);
props.put(propName, refSchema);
} else if (ModelUtils.isComposedSchema(prop)) {
if (prop.getAllOf() != null && prop.getAllOf().size() == 1 &&
!(((Schema) prop.getAllOf().get(0)).getType() == null ||
"object".equals(((Schema) prop.getAllOf().get(0)).getType()))) {
// allOf with only 1 type (non-model)
LOGGER.info("allOf schema used by the property `{}` replaced by its only item (a type)", propName);
props.put(propName, (Schema) prop.getAllOf().get(0));
}
}
}
}
// Check additionalProperties for inline models
if (schema.getAdditionalProperties() != null
&& schema.getAdditionalProperties() instanceof Schema> inner) {
String schemaName = resolveModelName(schema.getTitle(), modelPrefix + inlineSchemaOptions.get("MAP_ITEM_SUFFIX"));
// Recurse to create $refs for inner models
gatherInlineModels(inner, schemaName);
if (isModelNeeded(inner)) {
// If this schema should be split into its own model, do so
Schema refSchema = makeSchemaInComponents(schemaName, inner);
schema.setAdditionalProperties(refSchema);
}
}
if (schema.getItems() != null && schema.getItems().get$ref() == null) {
String schemaName = resolveModelName(schema.getTitle(), modelPrefix + "Enum");
// Recurse to create $refs for inner models
gatherInlineModels(schema.getItems(), schemaName);
if (isModelNeeded(schema.getItems())) {
// If this schema should be split into its own model, do so
Schema refSchema = makeSchemaInComponents(schemaName, schema.getItems());
schema.setAdditionalProperties(refSchema);
}
}
} else if (schema.getProperties() != null) {
// If non-object type is specified but also properties
LOGGER.error("Illegal schema found with non-object type combined with properties, no properties should be defined:\n {}", schema);
return;
} else if (schema.getAdditionalProperties() != null) {
// If non-object type is specified but also additionalProperties
LOGGER.error("Illegal schema found with non-object type combined with additionalProperties, no additionalProperties should be defined:\n {}", schema);
return;
}
// Check array items
if (schema instanceof ArraySchema array) {
var items = array.getItems();
if (items == null) {
LOGGER.error("Illegal schema found with array type but no items, items must be defined for array schemas:\n {}", schema);
return;
}
String schemaName = resolveModelName(items.getTitle(), modelPrefix + inlineSchemaOptions.get("ARRAY_ITEM_SUFFIX"));
// Recurse to create $refs for inner models
gatherInlineModels(items, schemaName);
if (isModelNeeded(items)) {
// If this schema should be split into its own model, do so
Schema refSchema = makeSchemaInComponents(schemaName, items);
array.setItems(refSchema);
}
}
// Check allOf, anyOf, oneOf for inline models
if (ModelUtils.isComposedSchema(schema)) {
if (schema.getAllOf() != null) {
List newAllOf = new ArrayList<>();
boolean atLeastOneModel = false;
for (Object inner : schema.getAllOf()) {
if (inner == null) {
continue;
}
normalizeArraySchema((Schema) inner);
String schemaName = resolveModelName(((Schema) inner).getTitle(), modelPrefix + "_allOf");
// Recurse to create $refs for inner models
gatherInlineModels((Schema) inner, schemaName);
if (isModelNeeded((Schema) inner)) {
if (Boolean.TRUE.equals(refactorAllOfInlineSchemas)) {
Schema refSchema = makeSchemaInComponents(schemaName, (Schema) inner);
newAllOf.add(refSchema); // replace with ref
atLeastOneModel = true;
} else { // do not refactor allOf inline schemas
newAllOf.add((Schema) inner);
atLeastOneModel = true;
}
} else {
newAllOf.add((Schema) inner);
}
}
if (atLeastOneModel) {
schema.setAllOf(newAllOf);
} else {
// allOf is just one or more types only so do not generate the inline allOf model
if (schema.getAllOf().size() > 1) {
LOGGER.warn("allOf schema `{}` containing multiple types (not model) is not supported at the moment.", schema.getName());
} else if (schema.getAllOf().size() != 1) {
// handle earlier in this function when looping through properties
LOGGER.error("allOf schema `{}` contains no items.", schema.getName());
}
}
}
if (schema.getAnyOf() != null) {
List newAnyOf = new ArrayList<>();
for (Object inner : schema.getAnyOf()) {
if (inner == null) {
continue;
}
normalizeArraySchema((Schema) inner);
String schemaName = resolveModelName(((Schema) inner).getTitle(), modelPrefix + "_anyOf");
// Recurse to create $refs for inner models
gatherInlineModels((Schema) inner, schemaName);
if (isModelNeeded((Schema) inner)) {
Schema refSchema = makeSchemaInComponents(schemaName, (Schema) inner);
newAnyOf.add(refSchema); // replace with ref
} else {
newAnyOf.add((Schema) inner);
}
}
schema.setAnyOf(newAnyOf);
}
if (schema.getOneOf() != null) {
List newOneOf = new ArrayList<>();
for (Object inner : schema.getOneOf()) {
if (inner == null) {
continue;
}
normalizeArraySchema((Schema) inner);
String schemaName = resolveModelName(((Schema) inner).getTitle(), modelPrefix + "_oneOf");
// Recurse to create $refs for inner models
gatherInlineModels((Schema) inner, schemaName);
if (isModelNeeded((Schema) inner)) {
Schema refSchema = makeSchemaInComponents(schemaName, (Schema) inner);
newOneOf.add(refSchema); // replace with ref
} else {
newOneOf.add((Schema) inner);
}
}
schema.setOneOf(newOneOf);
}
}
// Check not schema
if (schema.getNot() != null) {
Schema not = schema.getNot();
if (not != null) {
String schemaName = resolveModelName(schema.getTitle(), modelPrefix + "_not");
// Recurse to create $refs for inner models
gatherInlineModels(not, schemaName);
if (isModelNeeded(not)) {
Schema refSchema = makeSchemaInComponents(schemaName, not);
schema.setNot(refSchema);
}
}
}
}
private String resolveModelName(String title, String key) {
return title == null ? uniqueName(key) : uniqueName(title);
}
private String getInlineSchemaName(PathItem.HttpMethod httpVerb, String pathname) {
if (pathname.startsWith("/")) {
pathname = pathname.substring(1);
}
String name = pathname.replace('/', '_')
.replaceAll("[{}]", "");
if (httpVerb == PathItem.HttpMethod.DELETE) {
name += "_delete";
} else if (httpVerb == PathItem.HttpMethod.GET) {
name += "_get";
} else if (httpVerb == PathItem.HttpMethod.HEAD) {
name += "_head";
} else if (httpVerb == PathItem.HttpMethod.OPTIONS) {
name += "_options";
} else if (httpVerb == PathItem.HttpMethod.PATCH) {
name += "_patch";
} else if (httpVerb == PathItem.HttpMethod.POST) {
name += "_post";
} else if (httpVerb == PathItem.HttpMethod.PUT) {
name += "_put";
} else if (httpVerb == PathItem.HttpMethod.TRACE) {
name += "_trace";
}
return name;
}
/**
* Move schema to components (if new) and return $ref to schema or
* existing schema.
*
* @param name new schema name
* @param schema schema to move to components or find existing ref
*
* @return {@link Schema} $ref schema to new or existing schema
*/
private Schema makeSchemaInComponents(String name, Schema schema) {
String existing = matchGenerated(schema);
Schema refSchema;
if (existing != null) {
refSchema = new Schema().$ref(existing);
} else {
if (resolveInlineEnums && schema.getEnum() != null && !schema.getEnum().isEmpty()) {
LOGGER.warn("Model {} promoted to its own schema due to resolveInlineEnums=true", name);
}
name = addSchemas(name, schema);
refSchema = new Schema().$ref(name);
}
copyVendorExtensions(schema, refSchema);
return refSchema;
}
/**
* Copy vendor extensions from Model to another Model.
*
* @param source source property
* @param target target property
*/
private void copyVendorExtensions(Schema source, Schema target) {
Map vendorExtensions = source.getExtensions();
if (vendorExtensions == null) {
return;
}
for (String extName : vendorExtensions.keySet()) {
target.addExtension(extName, vendorExtensions.get(extName));
}
}
private String matchGenerated(Schema model) {
if (skipSchemaReuse) { // skip reusing schema
return null;
}
try {
String json = STRUCTURE_MAPPER.writeValueAsString(model);
if (generatedSignature.containsKey(json)) {
return generatedSignature.get(json);
}
} catch (JsonProcessingException e) {
LOGGER.warn("Error: {}", e.getMessage());
}
return null;
}
/**
* Return false if model can be represented by primitives e.g. string, object
* without properties, array or map of other model (model container), etc.
*
* Return true if a model should be generated e.g. object with properties,
* enum, oneOf, allOf, anyOf, etc.
*
* @param schema target schema
*/
private boolean isModelNeeded(Schema schema) {
return isModelNeeded(schema, new HashSet<>());
}
/**
* Return false if model can be represented by primitives e.g. string, object
* without properties, array or map of other model (model container), etc.
*
* Return true if a model should be generated e.g. object with properties,
* enum, oneOf, allOf, anyOf, etc.
*
* @param schema target schema
* @param visitedSchemas Visited schemas
*/
private boolean isModelNeeded(Schema schema, Set visitedSchemas) {
if (visitedSchemas.contains(schema)) { // circular reference
return true;
} else {
visitedSchemas.add(schema);
}
if (schema.getEnum() != null && !schema.getEnum().isEmpty()) {
return true;
}
if (schema.getType() == null || "object".equals(schema.getType())) {
// object or undeclared type with properties
if (schema.getProperties() != null && !schema.getProperties().isEmpty()) {
return true;
}
}
if (ModelUtils.isComposedSchema(schema)) {
// allOf, anyOf, oneOf
boolean isSingleAllOf = schema.getAllOf() != null && schema.getAllOf().size() == 1;
boolean isReadOnly = schema.getReadOnly() != null && schema.getReadOnly();
boolean isNullable = schema.getNullable() != null && schema.getNullable();
if (isSingleAllOf && (isReadOnly || isNullable)) {
// Check if this composed schema only contains an allOf and a readOnly or nullable.
var c = new ComposedSchema();
c.setAllOf(schema.getAllOf());
c.setReadOnly(schema.getReadOnly());
c.setNullable(schema.getNullable());
if (schema.equals(c)) {
return isModelNeeded((Schema) schema.getAllOf().get(0), visitedSchemas);
}
} else if (isSingleAllOf && StringUtils.isNotEmpty(((Schema) schema.getAllOf().get(0)).get$ref())) {
// single allOf and it's a ref
return isModelNeeded((Schema) schema.getAllOf().get(0), visitedSchemas);
}
if (schema.getAllOf() != null && !schema.getAllOf().isEmpty()) {
// check to ensure at least one of the allOf item is model
for (Object inner : schema.getAllOf()) {
if (isModelNeeded(ModelUtils.getReferencedSchema(openAPI, (Schema) inner), visitedSchemas)) {
return true;
}
}
// allOf items are all non-model (e.g. type: string) only
return false;
}
if (schema.getAnyOf() != null && !schema.getAnyOf().isEmpty()) {
return true;
}
return schema.getOneOf() != null && !schema.getOneOf().isEmpty();
}
return false;
}
/**
* Add the schemas to the components.
*
* @param name name of the inline schema
* @param schema inline schema
*
* @return the actual model name (based on inlineSchemaNameMapping if provided)
*/
private String addSchemas(String name, Schema schema) {
//check inlineSchemaNameMapping
if (inlineSchemaNameMapping.containsKey(name)) {
name = inlineSchemaNameMapping.get(name);
}
addGenerated(name, schema);
openAPI.getComponents().addSchemas(name, schema);
if (!name.equals(schema.getTitle()) && !inlineSchemaNameMappingValues.contains(name)) {
LOGGER.info("Inline schema created as {}. To have complete control of the model name, set the `title` field or use the modelNameMapping option (e.g. --model-name-mappings {}=NewModel,ModelA=NewModelA in CLI) or inlineSchemaNameMapping option (--inline-schema-name-mappings {}=NewModel,ModelA=NewModelA in CLI).", name, name, name);
}
uniqueNames.add(name);
return name;
}
private void addGenerated(String name, Schema model) {
try {
String json = STRUCTURE_MAPPER.writeValueAsString(model);
generatedSignature.put(json, name);
} catch (JsonProcessingException e) {
LOGGER.error("Error: {}", e.getMessage());
}
}
/**
* Generate a unique name for the input.
*
* @param name name to be processed to make sure it's unique
*/
private String uniqueName(final String name) {
if (openAPI.getComponents().getSchemas() == null) { // no schema has been created
return name;
}
String uniqueName = name;
int count = 0;
while (true) {
if (!openAPI.getComponents().getSchemas().containsKey(uniqueName) && !uniqueNames.contains(uniqueName)) {
return uniqueName;
}
uniqueName = name + "_" + ++count;
}
}
/**
* Flatten inline models in components.
*/
private void flattenComponents() {
Map models = openAPI.getComponents().getSchemas();
if (models == null) {
return;
}
var modelNames = new ArrayList<>(models.keySet());
for (String modelName : modelNames) {
Schema model = models.get(modelName);
if (model == null) {
continue;
}
normalizeArraySchema(model);
if (ModelUtils.isAnyOf(model)) { // contains anyOf only
gatherInlineModels(model, modelName);
} else if (ModelUtils.isOneOf(model)) { // contains oneOf only
gatherInlineModels(model, modelName);
} else if (ModelUtils.isComposedSchema(model)) {
// inline child schemas
flattenComposedChildren(modelName + "_allOf", model.getAllOf(), !Boolean.TRUE.equals(this.refactorAllOfInlineSchemas));
flattenComposedChildren(modelName + "_anyOf", model.getAnyOf(), false);
flattenComposedChildren(modelName + "_oneOf", model.getOneOf(), false);
} else {
gatherInlineModels(model, modelName);
}
}
}
private void flattenComposedChildren(String key, List children, boolean skipAllOfInlineSchemas) {
if (children == null || children.isEmpty()) {
return;
}
ListIterator listIterator = children.listIterator();
while (listIterator.hasNext()) {
Schema component = listIterator.next();
if ((component != null) &&
(component.get$ref() == null) &&
((component.getProperties() != null && !component.getProperties().isEmpty()) ||
(component.getEnum() != null && !component.getEnum().isEmpty()))) {
// If a `title` attribute is defined in the inline schema, codegen uses it to name the
// inline schema. Otherwise, we'll use the default naming such as InlineObject1, etc.
// We know that this is not the best way to name the model.
//
// Such naming strategy may result in issues. If the value of the 'title' attribute
// happens to match a schema defined elsewhere in the specification, 'innerModelName'
// will be the same as that other schema.
//
// To have complete control of the model naming, one can define the model separately
// instead of inline.
String innerModelName = resolveModelName(component.getTitle(), key);
Schema innerModel = modelFromProperty(openAPI, component, innerModelName);
// Recurse to create $refs for inner models
gatherInlineModels(innerModel, innerModelName);
if (!skipAllOfInlineSchemas) {
String existing = matchGenerated(innerModel);
if (existing == null) {
innerModelName = addSchemas(innerModelName, innerModel);
Schema schema = new Schema().$ref(innerModelName);
schema.setRequired(component.getRequired());
listIterator.set(schema);
} else {
Schema schema = new Schema().$ref(existing);
schema.setRequired(component.getRequired());
listIterator.set(schema);
}
} else {
LOGGER.debug("Inline allOf schema {} not refactored into a separate model using $ref.", innerModelName);
}
}
}
}
private Schema modelFromProperty(OpenAPI openAPI, Schema object, String path) {
String description = object.getDescription();
String example = null;
Object obj = object.getExample();
if (obj != null) {
example = obj.toString();
}
XML xml = object.getXml();
Map properties = object.getProperties();
// NOTE:
// No need to null check setters below. All defaults in the new'd Schema are null, so setting to null would just be a noop.
Schema model = new Schema();
model.setType(object.getType());
// Even though the `format` keyword typically applies to primitive types only,
// the JSON schema specification states `format` can be used for any model type instance
// including object types.
model.setFormat(object.getFormat());
if (object.getExample() != null) {
model.setExample(example);
}
model.setDescription(description);
model.setName(object.getName());
model.setXml(xml);
model.setRequired(object.getRequired());
model.setNullable(object.getNullable());
model.setEnum(object.getEnum());
model.setType(object.getType());
model.setDiscriminator(object.getDiscriminator());
model.setWriteOnly(object.getWriteOnly());
model.setUniqueItems(object.getUniqueItems());
model.setTitle(object.getTitle());
model.setReadOnly(object.getReadOnly());
model.setPattern(object.getPattern());
model.setNot(object.getNot());
model.setMinProperties(object.getMinProperties());
model.setMinLength(object.getMinLength());
model.setMinItems(object.getMinItems());
model.setMinimum(object.getMinimum());
model.setMaxProperties(object.getMaxProperties());
model.setMaxLength(object.getMaxLength());
model.setMaxItems(object.getMaxItems());
model.setMaximum(object.getMaximum());
model.setExternalDocs(object.getExternalDocs());
model.setExtensions(object.getExtensions());
model.setExclusiveMinimum(object.getExclusiveMinimum());
model.setExclusiveMaximum(object.getExclusiveMaximum());
// no need to set example again as it's set earlier
model.setDeprecated(object.getDeprecated());
if (properties != null) {
flattenProperties(openAPI, properties, path);
model.setProperties(properties);
}
return model;
}
private void flattenProperties(OpenAPI openAPI, Map properties, String path) {
if (properties == null) {
return;
}
Map propsToUpdate = new HashMap<>();
Map modelsToAdd = new HashMap<>();
for (Map.Entry propertiesEntry : properties.entrySet()) {
String key = propertiesEntry.getKey();
Schema property = propertiesEntry.getValue();
if (ModelUtils.isObjectSchema(property)) {
Schema op = property;
String modelName = resolveModelName(op.getTitle(), path + "_" + key);
Schema model = modelFromProperty(openAPI, op, modelName);
String existing = matchGenerated(model);
if (existing != null) {
Schema schema = new Schema().$ref(existing);
schema.setRequired(op.getRequired());
propsToUpdate.put(key, schema);
} else {
modelName = addSchemas(modelName, model);
Schema schema = new Schema().$ref(modelName);
schema.setRequired(op.getRequired());
propsToUpdate.put(key, schema);
modelsToAdd.put(modelName, model);
}
} else if (ModelUtils.isArraySchema(property)) {
Schema inner = ModelUtils.getSchemaItems(property);
if (ModelUtils.isObjectSchema(inner)) {
Schema op = inner;
if (op.getProperties() != null && op.getProperties().size() > 0) {
flattenProperties(openAPI, op.getProperties(), path);
String modelName = resolveModelName(op.getTitle(), path + "_" + key);
Schema innerModel = modelFromProperty(openAPI, op, modelName);
String existing = matchGenerated(innerModel);
if (existing != null) {
Schema schema = new Schema().$ref(existing);
schema.setRequired(op.getRequired());
property.setItems(schema);
} else {
modelName = addSchemas(modelName, innerModel);
Schema schema = new Schema().$ref(modelName);
schema.setRequired(op.getRequired());
property.setItems(schema);
}
}
} else if (ModelUtils.isComposedSchema(inner)) {
String innerModelName = resolveModelName(inner.getTitle(), path + "_" + key);
gatherInlineModels(inner, innerModelName);
innerModelName = addSchemas(innerModelName, inner);
Schema schema = new Schema().$ref(innerModelName);
schema.setRequired(inner.getRequired());
property.setItems(schema);
} else {
LOGGER.debug("Schema not yet handled in model resolver: {}", inner);
}
} else if (ModelUtils.isMapSchema(property)) {
Schema inner = ModelUtils.getAdditionalProperties(property);
if (ModelUtils.isObjectSchema(inner)) {
Schema op = inner;
if (op.getProperties() != null && op.getProperties().size() > 0) {
flattenProperties(openAPI, op.getProperties(), path);
String modelName = resolveModelName(op.getTitle(), path + "_" + key);
Schema innerModel = modelFromProperty(openAPI, op, modelName);
String existing = matchGenerated(innerModel);
if (existing != null) {
Schema schema = new Schema().$ref(existing);
schema.setRequired(op.getRequired());
property.setAdditionalProperties(schema);
} else {
modelName = addSchemas(modelName, innerModel);
Schema schema = new Schema().$ref(modelName);
schema.setRequired(op.getRequired());
property.setAdditionalProperties(schema);
}
}
} else if (ModelUtils.isComposedSchema(inner)) {
String innerModelName = resolveModelName(inner.getTitle(), path + "_" + key);
gatherInlineModels(inner, innerModelName);
innerModelName = addSchemas(innerModelName, inner);
Schema schema = new Schema().$ref(innerModelName);
schema.setRequired(inner.getRequired());
property.setAdditionalProperties(schema);
} else {
LOGGER.debug("Schema not yet handled in model resolver: {}", inner);
}
} else if (ModelUtils.isComposedSchema(property)) { // oneOf, anyOf, allOf etc
if (property.getAllOf() != null && property.getAllOf().size() == 1 // allOf with a single item
&& (property.getOneOf() == null || property.getOneOf().isEmpty()) // not oneOf
&& (property.getAnyOf() == null || property.getAnyOf().isEmpty()) // not anyOf
&& (property.getProperties() == null || property.getProperties().isEmpty())) { // no property
// don't do anything if it's allOf with a single item
LOGGER.debug("allOf with a single item (which can be handled by default codegen) skipped by inline model resolver: {}", property);
} else {
String propertyModelName = resolveModelName(property.getTitle(), path + "_" + key);
gatherInlineModels(property, propertyModelName);
propertyModelName = addSchemas(propertyModelName, property);
var schema = new Schema<>()
.$ref(propertyModelName)
.required(property.getRequired());
propsToUpdate.put(key, schema);
}
} else {
LOGGER.debug("Schema not yet handled in model resolver: {}", property);
}
}
if (!propsToUpdate.isEmpty()) {
properties.putAll(propsToUpdate);
}
for (var entry : modelsToAdd.entrySet()) {
openAPI.getComponents().addSchemas(entry.getKey(), entry.getValue());
this.addedModels.put(entry.getKey(), entry.getValue());
}
}
}