
org.openapitools.codegen.languages.ProtobufSchemaCodegen Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2019 OpenAPI-Generator Contributors (https://openapi-generator.tech)
*
* 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 org.openapitools.codegen.languages;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.MapSchema;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.Schema;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.*;
import org.openapitools.codegen.exceptions.ProtoBufIndexComputationException;
import org.openapitools.codegen.meta.GeneratorMetadata;
import org.openapitools.codegen.meta.Stability;
import org.openapitools.codegen.meta.features.DocumentationFeature;
import org.openapitools.codegen.meta.features.SecurityFeature;
import org.openapitools.codegen.meta.features.WireFormatFeature;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.ModelsMap;
import org.openapitools.codegen.model.OperationMap;
import org.openapitools.codegen.model.OperationsMap;
import org.openapitools.codegen.utils.ModelUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.google.common.base.CaseFormat;
import static org.openapitools.codegen.utils.StringUtils.*;
public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConfig {
private static final String IMPORT = "import";
private static final String IMPORTS = "imports";
private static final String ARRAY_SUFFIX = "Array";
private static final String MAP_SUFFIX = "Map";
public static final String NUMBERED_FIELD_NUMBER_LIST = "numberedFieldNumberList";
public static final String START_ENUMS_WITH_UNSPECIFIED = "startEnumsWithUnspecified";
public static final String ADD_JSON_NAME_ANNOTATION = "addJsonNameAnnotation";
public static final String WRAP_COMPLEX_TYPE = "wrapComplexType";
public static final String USE_SIMPLIFIED_ENUM_NAMES = "useSimplifiedEnumNames";
public static final String AGGREGATE_MODELS_NAME = "aggregateModelsName";
public static final String CUSTOM_OPTIONS_API = "customOptionsApi";
public static final String CUSTOM_OPTIONS_MODEL = "customOptionsModel";
public static final String SUPPORT_MULTIPLE_RESPONSES = "supportMultipleResponses";
private final Logger LOGGER = LoggerFactory.getLogger(ProtobufSchemaCodegen.class);
@Setter protected String packageName = "openapitools";
@Setter protected String aggregateModelsName = null;
@SuppressWarnings("unused")
@Setter protected String customOptionsApi = null;
@SuppressWarnings("unused")
@Setter protected String customOptionsModel = null;
private boolean numberedFieldNumberList = false;
private boolean startEnumsWithUnspecified = false;
private boolean addJsonNameAnnotation = false;
private boolean wrapComplexType = true;
private boolean useSimplifiedEnumNames = false;
private boolean supportMultipleResponses = true;
@Override
public CodegenType getTag() {
return CodegenType.SCHEMA;
}
@Override
public String toEnumName(CodegenProperty property) {
return StringUtils.capitalize(property.name);
}
@Override
public String getName() {
return "protobuf-schema";
}
@Override
public String getHelp() {
return "Generates gRPC and protocol buffer schema files (beta)";
}
public ProtobufSchemaCodegen() {
super();
generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata)
.stability(Stability.BETA)
.build();
modifyFeatureSet(features -> features
.includeDocumentationFeatures(DocumentationFeature.Readme)
.includeWireFormatFeatures(WireFormatFeature.PROTOBUF)
.wireFormatFeatures(EnumSet.of(WireFormatFeature.PROTOBUF))
.securityFeatures(EnumSet.noneOf(SecurityFeature.class))
);
outputFolder = "generated-code/protobuf-schema";
modelTemplateFiles.put("model.mustache", ".proto");
apiTemplateFiles.put("api.mustache", ".proto");
embeddedTemplateDir = templateDir = "protobuf-schema";
hideGenerationTimestamp = Boolean.TRUE;
modelPackage = "models";
apiPackage = "services";
defaultIncludes = new HashSet<>(
Arrays.asList(
"map",
"set",
"array")
);
languageSpecificPrimitives = new HashSet<>(
Arrays.asList(
"map",
"set",
"array",
"bool",
"bytes",
"string",
"int32",
"int64",
"uint32",
"uint64",
"sint32",
"sint64",
"fixed32",
"fixed64",
"sfixed32",
"sfixed64",
"float",
"double")
);
instantiationTypes.clear();
instantiationTypes.put("array", "repeat");
instantiationTypes.put("set", "repeat");
// ref: https://developers.google.com/protocol-buffers/docs/proto
typeMapping.clear();
typeMapping.put("set", "array");
typeMapping.put("array", "array");
typeMapping.put("map", "map");
typeMapping.put("integer", "int32");
typeMapping.put("long", "int64");
typeMapping.put("number", "float");
typeMapping.put("float", "float");
typeMapping.put("double", "double");
typeMapping.put("boolean", "bool");
typeMapping.put("string", "string");
typeMapping.put("UUID", "string");
typeMapping.put("URI", "string");
typeMapping.put("date", "string");
typeMapping.put("DateTime", "string");
typeMapping.put("password", "string");
// TODO fix file mapping
typeMapping.put("file", "string");
typeMapping.put("binary", "string");
typeMapping.put("ByteArray", "bytes");
typeMapping.put("object", "TODO_OBJECT_MAPPING");
importMapping.clear();
modelDocTemplateFiles.put("model_doc.mustache", ".md");
apiDocTemplateFiles.put("api_doc.mustache", ".md");
cliOptions.clear();
addSwitch(NUMBERED_FIELD_NUMBER_LIST, "Field numbers in order.", numberedFieldNumberList);
addSwitch(START_ENUMS_WITH_UNSPECIFIED, "Introduces \"UNSPECIFIED\" as the first element of enumerations.", startEnumsWithUnspecified);
addSwitch(ADD_JSON_NAME_ANNOTATION, "Append \"json_name\" annotation to message field when the specification name differs from the protobuf field name", addJsonNameAnnotation);
addSwitch(WRAP_COMPLEX_TYPE, "Generate Additional message for complex type", wrapComplexType);
addSwitch(USE_SIMPLIFIED_ENUM_NAMES, "Use a simple name for enums", useSimplifiedEnumNames);
addSwitch(SUPPORT_MULTIPLE_RESPONSES, "Support multiple responses", supportMultipleResponses);
addOption(AGGREGATE_MODELS_NAME, "Aggregated model filename. If set, all generated models will be combined into this single file.", null);
addOption(CUSTOM_OPTIONS_API, "Custom options for the api files.", null);
addOption(CUSTOM_OPTIONS_MODEL, "Custom options for the model files.", null);
}
@Override
public void processOpts() {
super.processOpts();
//apiTestTemplateFiles.put("api_test.mustache", ".proto");
//modelTestTemplateFiles.put("model_test.mustache", ".proto");
apiDocTemplateFiles.clear(); // TODO: add api doc template
modelDocTemplateFiles.clear(); // TODO: add model doc template
if (additionalProperties.containsKey(CodegenConstants.PACKAGE_NAME)) {
setPackageName((String) additionalProperties.get(CodegenConstants.PACKAGE_NAME));
} else {
additionalProperties.put(CodegenConstants.PACKAGE_NAME, packageName);
}
if (!additionalProperties.containsKey(CodegenConstants.API_PACKAGE)) {
additionalProperties.put(CodegenConstants.API_PACKAGE, apiPackage);
}
if (!additionalProperties.containsKey(CodegenConstants.MODEL_PACKAGE)) {
additionalProperties.put(CodegenConstants.MODEL_PACKAGE, modelPackage);
}
if (additionalProperties.containsKey(NUMBERED_FIELD_NUMBER_LIST)) {
this.numberedFieldNumberList = convertPropertyToBooleanAndWriteBack(NUMBERED_FIELD_NUMBER_LIST);
}
if (additionalProperties.containsKey(START_ENUMS_WITH_UNSPECIFIED)) {
this.startEnumsWithUnspecified = convertPropertyToBooleanAndWriteBack(START_ENUMS_WITH_UNSPECIFIED);
}
if (additionalProperties.containsKey(ADD_JSON_NAME_ANNOTATION)) {
this.addJsonNameAnnotation = convertPropertyToBooleanAndWriteBack(ADD_JSON_NAME_ANNOTATION);
}
if (additionalProperties.containsKey(WRAP_COMPLEX_TYPE)) {
this.wrapComplexType = convertPropertyToBooleanAndWriteBack(WRAP_COMPLEX_TYPE);
}
if (additionalProperties.containsKey(USE_SIMPLIFIED_ENUM_NAMES)) {
this.useSimplifiedEnumNames = convertPropertyToBooleanAndWriteBack(USE_SIMPLIFIED_ENUM_NAMES);
}
if (additionalProperties.containsKey(AGGREGATE_MODELS_NAME)) {
this.setAggregateModelsName((String) additionalProperties.get(AGGREGATE_MODELS_NAME));
}
if (additionalProperties.containsKey(CUSTOM_OPTIONS_API)) {
this.setCustomOptionsApi((String) additionalProperties.get(CUSTOM_OPTIONS_API));
}
if (additionalProperties.containsKey(CUSTOM_OPTIONS_MODEL)) {
this.setCustomOptionsModel((String) additionalProperties.get(CUSTOM_OPTIONS_MODEL));
}
if (additionalProperties.containsKey(this.SUPPORT_MULTIPLE_RESPONSES)) {
this.supportMultipleResponses = convertPropertyToBooleanAndWriteBack(SUPPORT_MULTIPLE_RESPONSES);
} else {
additionalProperties.put(this.SUPPORT_MULTIPLE_RESPONSES, this.supportMultipleResponses);
}
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
}
@Override
public String toOperationId(String operationId) {
// throw exception if method name is empty (should not occur as an auto-generated method name will be used)
if (StringUtils.isEmpty(operationId)) {
throw new RuntimeException("Empty method name (operationId) not allowed");
}
// method name cannot use reserved keyword, e.g. return
if (isReservedWord(operationId)) {
LOGGER.warn("{} (reserved word) cannot be used as method name. Renamed to {}", operationId, camelize(sanitizeName("call_" + operationId)));
operationId = "call_" + operationId;
}
return camelize(sanitizeName(operationId));
}
/**
* Creates an array schema from the provided object schema.
*
* @param objectSchema the schema of the object to be wrapped in an array schema
* @return the created array schema
*/
private Schema createArraySchema(Schema objectSchema) {
ArraySchema arraySchema = new ArraySchema();
arraySchema.items(objectSchema);
return arraySchema;
}
/**
* Creates a map schema from the provided object schema.
*
* @param objectSchema the schema of the object to be wrapped in a map schema
* @return the created map schema
*/
private Schema createMapSchema(Schema objectSchema) {
MapSchema mapSchema = new MapSchema();
mapSchema.additionalProperties(objectSchema);
return mapSchema;
}
/**
* Adds a new schema to the OpenAPI components.
*
* @param schema the schema to be added
* @param schemaName the name of the schema
* @param visitedSchema a set of schemas that have already been visited
* @return the reference schema
*/
private Schema addSchemas(Schema schema, String schemaName, Set visitedSchema) {
LOGGER.info("Generating new model: {}", schemaName);
ObjectSchema model = new ObjectSchema();
model.setName(schemaName);
Map properties = new HashMap<>();
properties.put(toVarName(schemaName), schema);
model.setProperties(properties);
Schema refSchema = new Schema();
refSchema.set$ref("#/components/schemas/" + schemaName);
refSchema.setName(schemaName);
visitedSchema.add(refSchema);
openAPI.getComponents().addSchemas(schemaName, model);
return refSchema;
}
/**
* Derive name from schema primitive type
*
* @param schema the schema to derive the name from
* @return the derived name
*/
private String getNameFromSchemaPrimitiveType(Schema schema) {
if (!ModelUtils.isPrimitiveType(schema)) return "";
if(ModelUtils.isNumberSchema(schema)) {
if(schema.getFormat() != null) {
return schema.getFormat();
} else if (typeMapping.get(schema.getType()) != null) {
return typeMapping.get(schema.getType());
}
}
return ModelUtils.getType(schema);
}
/**
* Recursively generates schemas for nested maps and arrays.
* @param schema the schema to be processed
* @param visitedSchemas a set of schemas that have already been visited
* @return the processed schema
*/
private Schema generateNestedSchema(Schema schema, Set visitedSchemas) {
if (visitedSchemas.contains(schema)) {
LOGGER.warn("Skipping recursive schema");
return schema;
}
if(ModelUtils.isArraySchema(schema)) {
Schema itemsSchema = ModelUtils.getSchemaItems(schema);
itemsSchema = ModelUtils.getReferencedSchema(openAPI, itemsSchema);
if(ModelUtils.isModel(itemsSchema)) {
String newSchemaName = ModelUtils.getSimpleRef(ModelUtils.getSchemaItems(schema).get$ref()) + ARRAY_SUFFIX;
return addSchemas(schema, newSchemaName, visitedSchemas);
}else if (ModelUtils.isPrimitiveType(itemsSchema)){
String newSchemaName = getNameFromSchemaPrimitiveType(itemsSchema) + ARRAY_SUFFIX;
return addSchemas(schema, newSchemaName, visitedSchemas);
} else {
Schema childSchema = generateNestedSchema(itemsSchema, visitedSchemas);
String newSchemaName = childSchema.getName() + ARRAY_SUFFIX;
Schema arrayModel = createArraySchema(childSchema);
return addSchemas(arrayModel, newSchemaName, visitedSchemas);
}
} else if(ModelUtils.isMapSchema(schema)) {
Schema mapValueSchema = ModelUtils.getAdditionalProperties(schema);
mapValueSchema = ModelUtils.getReferencedSchema(openAPI, mapValueSchema);
if(ModelUtils.isModel(mapValueSchema) ) {
String newSchemaName = ModelUtils.getSimpleRef(ModelUtils.getAdditionalProperties(schema).get$ref()) + MAP_SUFFIX;
return addSchemas(schema, newSchemaName, visitedSchemas);
}else if (ModelUtils.isPrimitiveType(mapValueSchema)){
String newSchemaName = getNameFromSchemaPrimitiveType(mapValueSchema) + MAP_SUFFIX;
return addSchemas(schema, newSchemaName, visitedSchemas);
} else {
Schema innerSchema = generateNestedSchema(mapValueSchema, visitedSchemas);
String newSchemaName = innerSchema.getName() + MAP_SUFFIX;
Schema mapModel = createMapSchema(innerSchema);
return addSchemas(mapModel, newSchemaName, visitedSchemas);
}
}
return schema;
}
/**
* Processes nested schemas for complex type(map, array, oneOf)
*
* @param schema the schema to be processed
* @param visitedSchemas a set of schemas that have already been visited
*/
private void processNestedSchemas(Schema schema, Set visitedSchemas) {
if (ModelUtils.isMapSchema(schema) && ModelUtils.getAdditionalProperties(schema) != null) {
Schema mapValueSchema = ModelUtils.getAdditionalProperties(schema);
mapValueSchema = ModelUtils.getReferencedSchema(openAPI, mapValueSchema);
if (ModelUtils.isArraySchema(mapValueSchema) || (ModelUtils.isMapSchema(mapValueSchema) && !ModelUtils.isModel(mapValueSchema))) {
Schema innerSchema = generateNestedSchema(mapValueSchema, visitedSchemas);
schema.setAdditionalProperties(innerSchema);
}
} else if (ModelUtils.isArraySchema(schema) && ModelUtils.getSchemaItems(schema) != null) {
Schema arrayItemSchema = ModelUtils.getSchemaItems(schema);
arrayItemSchema = ModelUtils.getReferencedSchema(openAPI, arrayItemSchema);
if ((ModelUtils.isMapSchema(arrayItemSchema) && !ModelUtils.isModel(arrayItemSchema)) || ModelUtils.isArraySchema(arrayItemSchema)) {
Schema innerSchema = generateNestedSchema(arrayItemSchema, visitedSchemas);
schema.setItems(innerSchema);
}
} else if (ModelUtils.isOneOf(schema) && schema.getOneOf() != null) {
List oneOfs = schema.getOneOf();
List newOneOfs = new ArrayList<>();
for (Schema oneOf : oneOfs) {
Schema oneOfSchema = ModelUtils.getReferencedSchema(openAPI, oneOf);
if (ModelUtils.isArraySchema(oneOfSchema)) {
Schema innerSchema = generateNestedSchema(oneOfSchema, visitedSchemas);
innerSchema.setTitle(oneOf.getTitle());
newOneOfs.add(innerSchema);
} else if (ModelUtils.isMapSchema(oneOfSchema) && !ModelUtils.isModel(oneOfSchema)) {
Schema innerSchema = generateNestedSchema(oneOfSchema, visitedSchemas);
innerSchema.setTitle(oneOf.getTitle());
newOneOfs.add(innerSchema);
} else {
newOneOfs.add(oneOf);
}
}
schema.setOneOf(newOneOfs);
}
}
/**
* Traverses models and properties to wrap nested schemas.
*/
private void wrapModels() {
Map models = openAPI.getComponents().getSchemas();
Set visitedSchema = new HashSet<>();
List modelNames = new ArrayList(models.keySet());
for (String modelName: modelNames) {
Schema schema = models.get(modelName);
processNestedSchemas(schema, visitedSchema);
if (ModelUtils.isModel(schema) && schema.getProperties() != null) {
Map properties = schema.getProperties();
for (Map.Entry propertyEntry : properties.entrySet()) {
Schema propertySchema = propertyEntry.getValue();
processNestedSchemas(propertySchema, visitedSchema);
}
} else if (ModelUtils.isAllOf(schema)) {
wrapComposedChildren(schema.getAllOf(), visitedSchema);
} else if (ModelUtils.isOneOf(schema)) {
wrapComposedChildren(schema.getOneOf(), visitedSchema);
} else if (ModelUtils.isAnyOf(schema)) {
wrapComposedChildren(schema.getAnyOf(), visitedSchema);
}
}
}
/**
* Traverses a composed schema and its properties to wrap nested schemas.
*
* @param children the list of child schemas to be processed
* @param visitedSchema a set of schemas that have already been visited
*/
private void wrapComposedChildren(List children, Set visitedSchema) {
if (children == null || children.isEmpty()) {
return;
}
for(Schema child: children) {
child = ModelUtils.getReferencedSchema(openAPI, child);
Map properties = child.getProperties();
if(properties == null || properties.isEmpty()) continue;
for(Map.Entry propertyEntry : properties.entrySet()) {
Schema propertySchema = propertyEntry.getValue();
processNestedSchemas(propertySchema, visitedSchema);
}
}
}
@Override
public void preprocessOpenAPI(OpenAPI openAPI) {
super.preprocessOpenAPI(openAPI);
if (wrapComplexType) {
wrapModels();
}
}
/**
* Adds prefix to the enum allowable values
* NOTE: Enum values use C++ scoping rules, meaning that enum values are siblings of their type, not children of it. Therefore, enum value must be unique
*
* @param allowableValues allowable values
* @param prefix added prefix
*/
public void addEnumValuesPrefix(Map allowableValues, String prefix) {
if (allowableValues.containsKey("enumVars")) {
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy