diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenProperty.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenProperty.java index 2b36fd5a2275..16edb14adf87 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenProperty.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenProperty.java @@ -62,6 +62,7 @@ public class CodegenProperty implements Cloneable { public boolean isWriteOnly; public boolean isNullable; public boolean isSelfReference; + public boolean isCircularReference; public List _enum; public Map allowableValues; public CodegenProperty items; @@ -498,6 +499,7 @@ public String toString() { sb.append(", isWriteOnly=").append(isWriteOnly); sb.append(", isNullable=").append(isNullable); sb.append(", isSelfReference=").append(isSelfReference); + sb.append(", isCircularReference=").append(isCircularReference); sb.append(", _enum=").append(_enum); sb.append(", allowableValues=").append(allowableValues); sb.append(", items=").append(items); @@ -558,6 +560,7 @@ public boolean equals(Object o) { isWriteOnly == that.isWriteOnly && isNullable == that.isNullable && isSelfReference == that.isSelfReference && + isCircularReference == that.isCircularReference && hasValidation == that.hasValidation && isInherited == that.isInherited && isXmlAttribute == that.isXmlAttribute && @@ -613,8 +616,9 @@ public int hashCode() { hasMoreNonReadOnly, isPrimitiveType, isModel, isContainer, isString, isNumeric, isInteger, isLong, isNumber, isFloat, isDouble, isByteArray, isBinary, isFile, isBoolean, isDate, isDateTime, isUuid, isUri, isEmail, isFreeFormObject, isListContainer, isMapContainer, isEnum, isReadOnly, - isWriteOnly, isNullable, isSelfReference, _enum, allowableValues, items, mostInnerItems, - vendorExtensions, hasValidation, isInherited, discriminatorValue, nameInCamelCase, nameInSnakeCase, - enumName, maxItems, minItems, isXmlAttribute, xmlPrefix, xmlName, xmlNamespace, isXmlWrapped); + isWriteOnly, isNullable, isSelfReference, isCircularReference, _enum, allowableValues, items, + mostInnerItems, vendorExtensions, hasValidation, isInherited, discriminatorValue, nameInCamelCase, + nameInSnakeCase, enumName, maxItems, minItems, isXmlAttribute, xmlPrefix, xmlName, xmlNamespace, + isXmlWrapped); } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 1099af7cd498..f35967ea2e99 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -328,10 +328,61 @@ public Map updateAllModels(Map objs) { } } } + setCircularReferences(allModels); return objs; } + public void setCircularReferences(Map models) { + final Map> dependencyMap = models.entrySet().stream() + .collect(Collectors.toMap(Entry::getKey, entry -> getModelDependencies(entry.getValue()))); + + models.keySet().forEach(name -> setCircularReferencesOnProperties(name, dependencyMap)); + } + + private List getModelDependencies(CodegenModel model) { + return model.getAllVars().stream() + .map(prop -> { + if (prop.isContainer) { + return prop.items.dataType == null ? null : prop; + } + return prop.dataType == null ? null : prop; + }) + .filter(prop -> prop != null) + .collect(Collectors.toList()); + } + + private void setCircularReferencesOnProperties(final String root, + final Map> dependencyMap) { + dependencyMap.getOrDefault(root, new ArrayList<>()).stream() + .forEach(prop -> { + final List unvisited = + Collections.singletonList(prop.isContainer ? prop.items.dataType : prop.dataType); + prop.isCircularReference = isCircularReference(root, + new HashSet<>(), + new ArrayList<>(unvisited), + dependencyMap); + }); + } + + private boolean isCircularReference(final String root, + final Set visited, + final List unvisited, + final Map> dependencyMap) { + for (int i = 0; i < unvisited.size(); i++) { + final String next = unvisited.get(i); + if (!visited.contains(next)) { + if (next.equals(root)) { + return true; + } + dependencyMap.getOrDefault(next, new ArrayList<>()) + .forEach(prop -> unvisited.add(prop.isContainer ? prop.items.dataType : prop.dataType)); + visited.add(next); + } + } + return false; + } + // override with any special post-processing @SuppressWarnings("static-method") public Map postProcessModels(Map objs) { diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ElmClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ElmClientCodegen.java index d192dd80010f..15bfcb010365 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ElmClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ElmClientCodegen.java @@ -347,6 +347,7 @@ public int compare(CodegenModel cm1, CodegenModel cm2) { }); } } + setCircularReferences(allModels); for (Map.Entry entry : objs.entrySet()) { Map inner = (Map) entry.getValue(); List> models = (List>) inner.get("models"); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java index 41e83880097d..0f25bf02e8c7 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java @@ -1113,4 +1113,55 @@ public void testConvertPropertyToBooleanAndWriteBack_String_blibb() { Assert.assertFalse(result); } } + + @Test + public void testCircularReferencesDetection() { + // given + DefaultCodegen codegen = new DefaultCodegen(); + final CodegenProperty inboundOut = new CodegenProperty(); + inboundOut.baseName = "out"; + inboundOut.dataType = "RoundA"; + final CodegenProperty roundANext = new CodegenProperty(); + roundANext.baseName = "next"; + roundANext.dataType = "RoundB"; + final CodegenProperty roundBNext = new CodegenProperty(); + roundBNext.baseName = "next"; + roundBNext.dataType = "RoundC"; + final CodegenProperty roundCNext = new CodegenProperty(); + roundCNext.baseName = "next"; + roundCNext.dataType = "RoundA"; + final CodegenProperty roundCOut = new CodegenProperty(); + roundCOut.baseName = "out"; + roundCOut.dataType = "Outbound"; + final CodegenModel inboundModel = new CodegenModel(); + inboundModel.setDataType("Inbound"); + inboundModel.setAllVars(Collections.singletonList(inboundOut)); + final CodegenModel roundAModel = new CodegenModel(); + roundAModel.setDataType("RoundA"); + roundAModel.setAllVars(Collections.singletonList(roundANext)); + final CodegenModel roundBModel = new CodegenModel(); + roundBModel.setDataType("RoundB"); + roundBModel.setAllVars(Collections.singletonList(roundBNext)); + final CodegenModel roundCModel = new CodegenModel(); + roundCModel.setDataType("RoundC"); + roundCModel.setAllVars(Arrays.asList(roundCNext, roundCOut)); + final CodegenModel outboundModel = new CodegenModel(); + outboundModel.setDataType("Outbound"); + final Map models = new HashMap<>(); + models.put("Inbound", inboundModel); + models.put("RoundA", roundAModel); + models.put("RoundB", roundBModel); + models.put("RoundC", roundCModel); + models.put("Outbound", outboundModel); + + // when + codegen.setCircularReferences(models); + + // then + Assert.assertFalse(inboundOut.isCircularReference); + Assert.assertTrue(roundANext.isCircularReference); + Assert.assertTrue(roundBNext.isCircularReference); + Assert.assertTrue(roundCNext.isCircularReference); + Assert.assertFalse(roundCOut.isCircularReference); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/recursion.yaml b/modules/openapi-generator/src/test/resources/3_0/recursion.yaml new file mode 100644 index 000000000000..dafd73e88723 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/recursion.yaml @@ -0,0 +1,33 @@ +openapi: 3.0.0 +info: + description: Test + version: 1.0.0 + title: OpenAPI +paths: + /foo: + post: + description: '' + responses: + '200': + description: Response + content: + application/json: + schema: + $ref: '#/components/schemas/Foo' +components: + schemas: + Foo: + type: object + properties: + foo: + $ref: '#/components/schemas/Foo' + Bar: + type: object + properties: + baz: + $ref: '#/components/schemas/Baz' + Baz: + type: object + properties: + bar: + $ref: '#/components/schemas/Bar'