Skip to content

Commit 1996d7e

Browse files
authored
[protobuf-schema] Add flag to handle complex type (#20915)
* Wrap Complex Type * update * Add java doc and update test * change default wrapComplexType to true * add protobuf-schema-config-complex to CI * add service proto to address CI failure
1 parent a2ee3a7 commit 1996d7e

File tree

19 files changed

+622
-16
lines changed

19 files changed

+622
-16
lines changed

.github/workflows/samples-protobuf.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jobs:
2020
sample:
2121
- 'samples/config/petstore/protobuf-schema/'
2222
- 'samples/config/petstore/protobuf-schema-config/'
23+
- 'samples/config/petstore/protobuf-schema-config-complex/'
2324
steps:
2425
- uses: actions/checkout@v4
2526
- name: Install Protocol Buffers Compiler
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
generatorName: protobuf-schema
2+
outputDir: samples/config/petstore/protobuf-schema-config-complex
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/protobuf/petstore-complex.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/protobuf-schema
5+
additionalProperties:
6+
packageName: petstore
7+
addJsonNameAnnotation: true
8+
numberedFieldNumberList: true
9+
startEnumsWithUnspecified: true

bin/configs/protobuf-schema-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ additionalProperties:
77
addJsonNameAnnotation: true
88
numberedFieldNumberList: true
99
startEnumsWithUnspecified: true
10+
wrapComplexType: false

docs/generators/protobuf-schema.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
2121
|addJsonNameAnnotation|Append "json_name" annotation to message field when the specification name differs from the protobuf field name| |false|
2222
|numberedFieldNumberList|Field numbers in order.| |false|
2323
|startEnumsWithUnspecified|Introduces "UNSPECIFIED" as the first element of enumerations.| |false|
24+
|wrapComplexType|Generate Additional message for complex type| |true|
2425

2526
## IMPORT MAPPING
2627

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ProtobufSchemaCodegen.java

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
package org.openapitools.codegen.languages;
1818

19+
import io.swagger.v3.oas.models.OpenAPI;
20+
import io.swagger.v3.oas.models.media.ArraySchema;
21+
import io.swagger.v3.oas.models.media.MapSchema;
22+
import io.swagger.v3.oas.models.media.ObjectSchema;
1923
import io.swagger.v3.oas.models.media.Schema;
2024
import lombok.Setter;
2125
import org.apache.commons.lang3.StringUtils;
@@ -58,6 +62,8 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf
5862

5963
public static final String ADD_JSON_NAME_ANNOTATION = "addJsonNameAnnotation";
6064

65+
public static final String WRAP_COMPLEX_TYPE = "wrapComplexType";
66+
6167
private final Logger LOGGER = LoggerFactory.getLogger(ProtobufSchemaCodegen.class);
6268

6369
@Setter protected String packageName = "openapitools";
@@ -68,6 +74,8 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf
6874

6975
private boolean addJsonNameAnnotation = false;
7076

77+
private boolean wrapComplexType = true;
78+
7179
@Override
7280
public CodegenType getTag() {
7381
return CodegenType.SCHEMA;
@@ -177,6 +185,7 @@ public ProtobufSchemaCodegen() {
177185
addSwitch(NUMBERED_FIELD_NUMBER_LIST, "Field numbers in order.", numberedFieldNumberList);
178186
addSwitch(START_ENUMS_WITH_UNSPECIFIED, "Introduces \"UNSPECIFIED\" as the first element of enumerations.", startEnumsWithUnspecified);
179187
addSwitch(ADD_JSON_NAME_ANNOTATION, "Append \"json_name\" annotation to message field when the specification name differs from the protobuf field name", addJsonNameAnnotation);
188+
addSwitch(WRAP_COMPLEX_TYPE, "Generate Additional message for complex type", wrapComplexType);
180189
}
181190

182191
@Override
@@ -215,6 +224,10 @@ public void processOpts() {
215224
this.addJsonNameAnnotation = convertPropertyToBooleanAndWriteBack(ADD_JSON_NAME_ANNOTATION);
216225
}
217226

227+
if (additionalProperties.containsKey(this.WRAP_COMPLEX_TYPE)) {
228+
this.wrapComplexType = convertPropertyToBooleanAndWriteBack(WRAP_COMPLEX_TYPE);
229+
}
230+
218231
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
219232
}
220233

@@ -234,6 +247,224 @@ public String toOperationId(String operationId) {
234247
return camelize(sanitizeName(operationId));
235248
}
236249

250+
/**
251+
* Creates an array schema from the provided object schema.
252+
*
253+
* @param objectSchema the schema of the object to be wrapped in an array schema
254+
* @return the created array schema
255+
*/
256+
private Schema createArraySchema(Schema objectSchema) {
257+
ArraySchema arraySchema = new ArraySchema();
258+
arraySchema.items(objectSchema);
259+
return arraySchema;
260+
}
261+
262+
263+
/**
264+
* Creates a map schema from the provided object schema.
265+
*
266+
* @param objectSchema the schema of the object to be wrapped in a map schema
267+
* @return the created map schema
268+
*/
269+
private Schema createMapSchema(Schema objectSchema) {
270+
MapSchema mapSchema = new MapSchema();
271+
mapSchema.additionalProperties(objectSchema);
272+
return mapSchema;
273+
}
274+
275+
/**
276+
* Adds a new schema to the OpenAPI components.
277+
*
278+
* @param schema the schema to be added
279+
* @param schemaName the name of the schema
280+
* @param visitedSchema a set of schemas that have already been visited
281+
* @return the reference schema
282+
*/
283+
private Schema addSchemas(Schema schema, String schemaName, Set<Schema> visitedSchema) {
284+
LOGGER.info("Generating new model: {}", schemaName);
285+
286+
ObjectSchema model = new ObjectSchema();
287+
model.setName(schemaName);
288+
289+
Map<String, Schema> properties = new HashMap<>();
290+
properties.put(toVarName(schemaName), schema);
291+
model.setProperties(properties);
292+
293+
Schema refSchema = new Schema();
294+
refSchema.set$ref("#/components/schemas/" + schemaName);
295+
refSchema.setName(schemaName);
296+
297+
visitedSchema.add(refSchema);
298+
299+
openAPI.getComponents().addSchemas(schemaName, model);
300+
301+
return refSchema;
302+
}
303+
304+
/**
305+
* Derive name from schema primitive type
306+
*
307+
* @param schema the schema to derive the name from
308+
* @return the derived name
309+
*/
310+
private String getNameFromSchemaPrimitiveType(Schema schema) {
311+
if (!ModelUtils.isPrimitiveType(schema)) return "";
312+
if(ModelUtils.isNumberSchema(schema)) {
313+
if(schema.getFormat() != null) {
314+
return schema.getFormat();
315+
} else if (typeMapping.get(schema.getType()) != null) {
316+
return typeMapping.get(schema.getType());
317+
}
318+
}
319+
return ModelUtils.getType(schema);
320+
}
321+
322+
/**
323+
* Recursively generates schemas for nested maps and arrays.
324+
* @param schema the schema to be processed
325+
* @param visitedSchemas a set of schemas that have already been visited
326+
* @return the processed schema
327+
*/
328+
private Schema generateNestedSchema(Schema schema, Set<Schema> visitedSchemas) {
329+
if (visitedSchemas.contains(schema)) {
330+
LOGGER.warn("Skipping recursive schema");
331+
return schema;
332+
}
333+
334+
if(ModelUtils.isArraySchema(schema)) {
335+
Schema itemsSchema = ModelUtils.getSchemaItems(schema);
336+
itemsSchema = ModelUtils.getReferencedSchema(openAPI, itemsSchema);
337+
if(ModelUtils.isModel(itemsSchema)) {
338+
String newSchemaName = ModelUtils.getSimpleRef(ModelUtils.getSchemaItems(schema).get$ref()) + ARRAY_SUFFIX;
339+
return addSchemas(schema, newSchemaName, visitedSchemas);
340+
}else if (ModelUtils.isPrimitiveType(itemsSchema)){
341+
String newSchemaName = getNameFromSchemaPrimitiveType(itemsSchema) + ARRAY_SUFFIX;
342+
return addSchemas(schema, newSchemaName, visitedSchemas);
343+
} else {
344+
Schema childSchema = generateNestedSchema(itemsSchema, visitedSchemas);
345+
String newSchemaName = childSchema.getName() + ARRAY_SUFFIX;
346+
Schema arrayModel = createArraySchema(childSchema);
347+
return addSchemas(arrayModel, newSchemaName, visitedSchemas);
348+
}
349+
} else if(ModelUtils.isMapSchema(schema)) {
350+
Schema mapValueSchema = ModelUtils.getAdditionalProperties(schema);
351+
mapValueSchema = ModelUtils.getReferencedSchema(openAPI, mapValueSchema);
352+
if(ModelUtils.isModel(mapValueSchema) ) {
353+
String newSchemaName = ModelUtils.getSimpleRef(ModelUtils.getAdditionalProperties(schema).get$ref()) + MAP_SUFFIX;
354+
return addSchemas(schema, newSchemaName, visitedSchemas);
355+
}else if (ModelUtils.isPrimitiveType(mapValueSchema)){
356+
String newSchemaName = getNameFromSchemaPrimitiveType(mapValueSchema) + MAP_SUFFIX;
357+
return addSchemas(schema, newSchemaName, visitedSchemas);
358+
} else {
359+
Schema innerSchema = generateNestedSchema(mapValueSchema, visitedSchemas);
360+
String newSchemaName = innerSchema.getName() + MAP_SUFFIX;
361+
Schema mapModel = createMapSchema(innerSchema);
362+
return addSchemas(mapModel, newSchemaName, visitedSchemas);
363+
}
364+
}
365+
return schema;
366+
}
367+
368+
/**
369+
* Processes nested schemas for complex type(map, array, oneOf)
370+
*
371+
* @param schema the schema to be processed
372+
* @param visitedSchemas a set of schemas that have already been visited
373+
*/
374+
private void processNestedSchemas(Schema schema, Set<Schema> visitedSchemas) {
375+
if (ModelUtils.isMapSchema(schema) && ModelUtils.getAdditionalProperties(schema) != null) {
376+
Schema mapValueSchema = ModelUtils.getAdditionalProperties(schema);
377+
mapValueSchema = ModelUtils.getReferencedSchema(openAPI, mapValueSchema);
378+
if (ModelUtils.isArraySchema(mapValueSchema) || ModelUtils.isMapSchema(mapValueSchema)) {
379+
Schema innerSchema = generateNestedSchema(mapValueSchema, visitedSchemas);
380+
schema.setAdditionalProperties(innerSchema);
381+
382+
}
383+
} else if (ModelUtils.isArraySchema(schema) && ModelUtils.getSchemaItems(schema) != null) {
384+
Schema arrayItemSchema = ModelUtils.getSchemaItems(schema);
385+
arrayItemSchema = ModelUtils.getReferencedSchema(openAPI, arrayItemSchema);
386+
if (ModelUtils.isMapSchema(arrayItemSchema) || ModelUtils.isArraySchema(arrayItemSchema)) {
387+
Schema innerSchema = generateNestedSchema(arrayItemSchema, visitedSchemas);
388+
schema.setItems(innerSchema);
389+
}
390+
} else if (ModelUtils.isOneOf(schema) && schema.getOneOf() != null) {
391+
List<Schema> oneOfs = schema.getOneOf();
392+
List<Schema> newOneOfs = new ArrayList<>();
393+
for (Schema oneOf : oneOfs) {
394+
Schema oneOfSchema = ModelUtils.getReferencedSchema(openAPI, oneOf);
395+
if (ModelUtils.isArraySchema(oneOfSchema)) {
396+
Schema innerSchema = generateNestedSchema(oneOfSchema, visitedSchemas);
397+
innerSchema.setTitle(oneOf.getTitle());
398+
newOneOfs.add(innerSchema);
399+
} else if (ModelUtils.isMapSchema(oneOfSchema)) {
400+
Schema innerSchema = generateNestedSchema(oneOfSchema, visitedSchemas);
401+
innerSchema.setTitle(oneOf.getTitle());
402+
newOneOfs.add(innerSchema);
403+
} else {
404+
newOneOfs.add(oneOf);
405+
}
406+
}
407+
schema.setOneOf(newOneOfs);
408+
}
409+
}
410+
411+
/**
412+
* Traverses models and properties to wrap nested schemas.
413+
*/
414+
private void wrapModels() {
415+
Map<String, Schema> models = openAPI.getComponents().getSchemas();
416+
Set<Schema> visitedSchema = new HashSet<>();
417+
List<String> modelNames = new ArrayList<String>(models.keySet());
418+
for (String modelName: modelNames) {
419+
Schema schema = models.get(modelName);
420+
processNestedSchemas(schema, visitedSchema);
421+
if (ModelUtils.isModel(schema) && schema.getProperties() != null) {
422+
Map<String, Schema> properties = schema.getProperties();
423+
for (Map.Entry<String, Schema> propertyEntry : properties.entrySet()) {
424+
Schema propertySchema = propertyEntry.getValue();
425+
processNestedSchemas(propertySchema, visitedSchema);
426+
}
427+
} else if (ModelUtils.isAllOf(schema)) {
428+
wrapComposedChildren(schema.getAllOf(), visitedSchema);
429+
} else if (ModelUtils.isOneOf(schema)) {
430+
wrapComposedChildren(schema.getOneOf(), visitedSchema);
431+
} else if (ModelUtils.isAnyOf(schema)) {
432+
wrapComposedChildren(schema.getAnyOf(), visitedSchema);
433+
}
434+
435+
}
436+
}
437+
438+
/**
439+
* Traverses a composed schema and its properties to wrap nested schemas.
440+
*
441+
* @param children the list of child schemas to be processed
442+
* @param visitedSchema a set of schemas that have already been visited
443+
*/
444+
private void wrapComposedChildren(List<Schema> children, Set<Schema> visitedSchema) {
445+
if (children == null || children.isEmpty()) {
446+
return;
447+
}
448+
for(Schema child: children) {
449+
child = ModelUtils.getReferencedSchema(openAPI, child);
450+
Map<String, Schema> properties = child.getProperties();
451+
if(properties == null || properties.isEmpty()) continue;
452+
for(Map.Entry<String, Schema> propertyEntry : properties.entrySet()) {
453+
Schema propertySchema = propertyEntry.getValue();
454+
processNestedSchemas(propertySchema, visitedSchema);
455+
}
456+
}
457+
}
458+
459+
@Override
460+
public void preprocessOpenAPI(OpenAPI openAPI) {
461+
super.preprocessOpenAPI(openAPI);
462+
if (wrapComplexType) {
463+
wrapModels();
464+
}
465+
}
466+
467+
237468
/**
238469
* Adds prefix to the enum allowable values
239470
* 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

0 commit comments

Comments
 (0)