1616
1717package 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 ;
1923import io .swagger .v3 .oas .models .media .Schema ;
2024import lombok .Setter ;
2125import 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