diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java index ca3528648..9eee7fa1d 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java @@ -72,7 +72,7 @@ public class OpenAPIComponentMapper { private final Components components; private final List diagnostics; - private final List visitedTypeDefinitionNames = new ArrayList<>(); + private final HashSet visitedTypeDefinitionNames = new HashSet<>(); public OpenAPIComponentMapper(Components components) { this.components = components; @@ -117,7 +117,10 @@ public void createComponentSchema(Map schema, TypeSymbol typeSym type.getName().orElseThrow().trim()))); components.setSchemas(schema); TypeReferenceTypeSymbol referredType = (TypeReferenceTypeSymbol) type; - createComponentSchema(schema, referredType); + if (!visitedTypeDefinitionNames.contains(componentName)) { + visitedTypeDefinitionNames.add(componentName); + createComponentSchema(schema, referredType); + } break; case STRING: schema.put(componentName, new StringSchema().description(typeDoc)); @@ -147,14 +150,19 @@ public void createComponentSchema(Map schema, TypeSymbol typeSym components.setSchemas(schema); break; case UNION: - if (typeRef.definition() instanceof EnumSymbol) { - EnumSymbol enumSymbol = (EnumSymbol) typeRef.definition(); - Schema enumSchema = mapEnumValues(enumSymbol); - schema.put(componentName, enumSchema.description(typeDoc)); - components.setSchemas(schema); - } else { - Schema unionSchema = handleUnionType((UnionTypeSymbol) type, new Schema<>(), componentName); - schema.put(componentName, unionSchema.description(typeDoc)); + if (!visitedTypeDefinitionNames.contains(componentName)) { + visitedTypeDefinitionNames.add(componentName); + if (typeRef.definition() instanceof EnumSymbol) { + EnumSymbol enumSymbol = (EnumSymbol) typeRef.definition(); + Schema enumSchema = mapEnumValues(enumSymbol); + schema.put(componentName, enumSchema.description(typeDoc)); + } else { + Schema unionSchema = handleUnionType((UnionTypeSymbol) type, new Schema<>(), componentName); + schema.put(componentName, unionSchema.description(typeDoc)); + } + if (components.getSchemas() != null) { + schema.putAll(components.getSchemas()); + } components.setSchemas(schema); } break; @@ -217,11 +225,10 @@ private void handleRecordTypeSymbol(RecordTypeSymbol recordTypeSymbol, Map typeInclusions = recordTypeSymbol.typeInclusions(); Map rfields = recordTypeSymbol.fieldDescriptors(); - HashSet unionKeys = new HashSet<>(rfields.keySet()); if (typeInclusions.isEmpty()) { generateObjectSchemaFromRecordFields(schema, componentName, rfields, apiDocs); } else { - mapTypeInclusionToAllOfSchema(schema, componentName, typeInclusions, rfields, unionKeys, apiDocs); + mapTypeInclusionToAllOfSchema(schema, componentName, recordTypeSymbol, apiDocs); } } @@ -229,7 +236,6 @@ private void handleRecordTypeSymbol(RecordTypeSymbol recordTypeSymbol, Map getRecordFieldsAPIDocsMap(TypeReferenceTypeSymbol typeSymbol, String componentName) { - Map apiDocs = new LinkedHashMap<>(); Symbol recordSymbol = typeSymbol.definition(); Optional documentation = ((Documentable) recordSymbol).documentation(); @@ -258,10 +264,12 @@ private Map getRecordFieldsAPIDocsMap(TypeReferenceTypeSymbol ty /** * This function is to map the ballerina typeInclusion to OAS allOf composedSchema. */ - private void mapTypeInclusionToAllOfSchema(Map schema, - String componentName, List typeInclusions, Map rfields, HashSet unionKeys, Map apiDocs) { + private void mapTypeInclusionToAllOfSchema(Map schema, String componentName, + RecordTypeSymbol recordTypeSymbol, Map apiDocs) { + List typeInclusions = recordTypeSymbol.typeInclusions(); + Map recordFields = recordTypeSymbol.fieldDescriptors(); + HashSet recordFieldNames = new HashSet<>(recordFields.keySet()); // Map to allOF need to check the status code inclusion there ComposedSchema allOfSchema = new ComposedSchema(); // Set schema @@ -273,11 +281,12 @@ private void mapTypeInclusionToAllOfSchema(Map schema, allOfSchemaList.add(referenceSchema); if (typeInclusion.typeKind().equals(TypeDescKind.TYPE_REFERENCE)) { TypeReferenceTypeSymbol typeRecord = (TypeReferenceTypeSymbol) typeInclusion; - if (typeRecord.typeDescriptor() instanceof RecordTypeSymbol) { + if (typeRecord.typeDescriptor() instanceof RecordTypeSymbol && + !isSameRecord(typeInclusionName, typeRecord)) { RecordTypeSymbol typeInclusionRecord = (RecordTypeSymbol) typeRecord.typeDescriptor(); Map tInFields = typeInclusionRecord.fieldDescriptors(); - unionKeys.addAll(tInFields.keySet()); - unionKeys.removeAll(tInFields.keySet()); + recordFieldNames.addAll(tInFields.keySet()); + recordFieldNames.removeAll(tInFields.keySet()); generateObjectSchemaFromRecordFields(schema, typeInclusionName, tInFields, apiDocs); // Update the schema value schema = this.components.getSchemas(); @@ -285,7 +294,7 @@ private void mapTypeInclusionToAllOfSchema(Map schema, } } Map filteredField = new LinkedHashMap<>(); - rfields.forEach((key1, value) -> unionKeys.stream().filter(key -> + recordFields.forEach((key1, value) -> recordFieldNames.stream().filter(key -> ConverterCommonUtils.unescapeIdentifier(key1.trim()). equals(ConverterCommonUtils.unescapeIdentifier(key))).forEach(key -> filteredField.put(ConverterCommonUtils.unescapeIdentifier(key1), value))); @@ -295,6 +304,9 @@ private void mapTypeInclusionToAllOfSchema(Map schema, if (schema != null && !schema.containsKey(componentName)) { // Set properties for the schema schema.put(componentName, allOfSchema); + if (this.components.getSchemas() != null) { + schema.putAll(this.components.getSchemas()); + } this.components.setSchemas(schema); } else if (schema == null) { schema = new LinkedHashMap<>(); @@ -331,8 +343,9 @@ private ObjectSchema generateObjectSchemaFromRecordFields(Map sc if (fieldTypeKind == TypeDescKind.TYPE_REFERENCE) { TypeReferenceTypeSymbol typeReference = (TypeReferenceTypeSymbol) field.getValue().typeDescriptor(); - property = handleTypeReference(schema, typeReference, property, isSameRecord(componentName, - typeReference)); + property = handleTypeReference(schema, typeReference, property, + isSameRecord(ConverterCommonUtils.unescapeIdentifier( + typeReference.definition().getName().get()), typeReference)); schema = components.getSchemas(); } else if (fieldTypeKind == TypeDescKind.UNION) { property = handleUnionType((UnionTypeSymbol) field.getValue().typeDescriptor(), property, @@ -360,12 +373,16 @@ private ObjectSchema generateObjectSchemaFromRecordFields(Map sc if (componentName != null && schema != null && !schema.containsKey(componentName)) { // Set properties for the schema schema.put(componentName, componentSchema); + if (this.components.getSchemas() != null) { + schema.putAll(this.components.getSchemas()); + } this.components.setSchemas(schema); } else if (schema == null && componentName != null) { schema = new LinkedHashMap<>(); schema.put(componentName, componentSchema); this.components.setSchemas(schema); } + visitedTypeDefinitionNames.add(componentName); return componentSchema; } @@ -431,10 +448,10 @@ private Schema handleUnionType(UnionTypeSymbol unionType, Schema property, Strin } property = ConverterCommonUtils.getOpenApiSchema(union.typeKind().getName().trim()); TypeReferenceTypeSymbol typeReferenceTypeSymbol = (TypeReferenceTypeSymbol) union; - property = handleTypeReference(this.components.getSchemas(), typeReferenceTypeSymbol, property, + property = handleTypeReference(components.getSchemas(), typeReferenceTypeSymbol, property, isSameRecord(parentComponentName, typeReferenceTypeSymbol)); + visitedTypeDefinitionNames.add(typeReferenceTypeSymbol.getName().get()); properties.add(property); - // TODO: uncomment after fixing ballerina lang union type handling issue } else if (union.typeKind() == TypeDescKind.UNION) { property = handleUnionType((UnionTypeSymbol) union, property, parentComponentName); properties.add(property); @@ -447,9 +464,10 @@ private Schema handleUnionType(UnionTypeSymbol unionType, Schema property, Strin Schema openApiSchema = ConverterCommonUtils.getOpenApiSchema(typeDescKind.getName()); property = new ObjectSchema().additionalProperties(openApiSchema); properties.add(property); - Map schemas = components.getSchemas(); - if (schemas != null) { + if (components.getSchemas() != null) { + Map schemas = components.getSchemas(); schemas.put(parentComponentName, property); + components.setSchemas(schemas); } else { Map schema = new HashMap<>(); schema.put(parentComponentName, property); @@ -460,7 +478,6 @@ private Schema handleUnionType(UnionTypeSymbol unionType, Schema property, Strin properties.add(property); } } - property = generateOneOfSchema(property, properties); if (nullable) { property.setNullable(true); @@ -514,6 +531,7 @@ private Schema mapEnumValues(EnumSymbol enumSymbol) { */ private ArraySchema mapArrayToArraySchema(Map schema, TypeSymbol symbol, String componentName) { + visitedTypeDefinitionNames.add(componentName); ArraySchema property = new ArraySchema(); int arrayDimensions = 0; while (symbol instanceof ArrayTypeSymbol) { @@ -594,17 +612,17 @@ private Schema getSchemaForUnionType(UnionTypeSymbol symbol, Schema symbolProper /** * This util function is to handle the type reference symbol is record type or enum type. */ - private Schema getSchemaForTypeReferenceSymbol(TypeSymbol arrayType, Schema symbolProperty, String componentName, - Map schema) { + private Schema getSchemaForTypeReferenceSymbol(TypeSymbol referenceType, Schema symbolProperty, + String componentName, Map schema) { - if (((TypeReferenceTypeSymbol) arrayType).definition().kind() == SymbolKind.ENUM) { - TypeReferenceTypeSymbol typeRefEnum = (TypeReferenceTypeSymbol) arrayType; + if (((TypeReferenceTypeSymbol) referenceType).definition().kind() == SymbolKind.ENUM) { + TypeReferenceTypeSymbol typeRefEnum = (TypeReferenceTypeSymbol) referenceType; EnumSymbol enumSymbol = (EnumSymbol) typeRefEnum.definition(); symbolProperty = mapEnumValues(enumSymbol); } else { symbolProperty.set$ref(ConverterCommonUtils.unescapeIdentifier( - arrayType.getName().orElseThrow().trim())); - TypeReferenceTypeSymbol typeRecord = (TypeReferenceTypeSymbol) arrayType; + referenceType.getName().orElseThrow().trim())); + TypeReferenceTypeSymbol typeRecord = (TypeReferenceTypeSymbol) referenceType; if (!isSameRecord(componentName, typeRecord)) { createComponentSchema(schema, typeRecord); } @@ -624,5 +642,4 @@ private ArraySchema handleArray(int arrayDimensions, Schema property, ArraySchem } return arrayProperty; } - } diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/RecordTests.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/RecordTests.java index f9414f195..90ceb50cf 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/RecordTests.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/RecordTests.java @@ -101,6 +101,24 @@ public void testReadOnlyRecord() throws IOException { TestUtils.compareWithGeneratedFile(ballerinaFilePath, "record/with_read_only_keyword.yaml"); } + @Test(description = "Test for records having cyclic dependencies and same record inclusions") + public void testRecordsWithCyclicDependenciesIncludingSameTypeInclusion() throws IOException { + Path ballerinaFilePath = RES_DIR.resolve("record/cyclic_record_with_typeInclusion.bal"); + TestUtils.compareWithGeneratedFile(ballerinaFilePath, "record/cyclic_record_with_typeInclusion.yaml"); + } + + @Test(description = "Test for record type definitions with interdependencies") + public void testInterdependenceRecordWithTypeRef() throws IOException { + Path ballerinaFilePath = RES_DIR.resolve("record/typeref_records_with_interdependency.bal"); + TestUtils.compareWithGeneratedFile(ballerinaFilePath, "record/typeref_records_with_interdependency.yaml"); + } + + @Test(description = "Test for union type with interdependent record members") + public void testInterdependenceRecordWithUnionType() throws IOException { + Path ballerinaFilePath = RES_DIR.resolve("record/union_records_with_interdependency.bal"); + TestUtils.compareWithGeneratedFile(ballerinaFilePath, "record/union_records_with_interdependency.yaml"); + } + @AfterMethod public void cleanUp() { TestUtils.deleteDirectory(this.tempDir); diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/record/cyclic_record_with_typeInclusion.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/record/cyclic_record_with_typeInclusion.yaml new file mode 100644 index 000000000..05316ffd1 --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/record/cyclic_record_with_typeInclusion.yaml @@ -0,0 +1,75 @@ +openapi: 3.0.1 +info: + title: PayloadV + version: 0.0.0 +servers: + - url: "{server}:{port}/payloadV" + variables: + server: + default: http://localhost + port: + default: "9090" +paths: + /fhir/r4/Patient/{id}: + get: + operationId: getFhirR4PatientId + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Ok + content: + application/fhir+json: + schema: + $ref: '#/components/schemas/Patient' + application/fhir+xml: + schema: + $ref: '#/components/schemas/Patient' +components: + schemas: + Identifier: + allOf: + - $ref: '#/components/schemas/Element' + - type: object + properties: + value: + type: string + assigner: + $ref: '#/components/schemas/Reference' + id: + type: string + element: + type: integer + format: int64 + Reference: + allOf: + - $ref: '#/components/schemas/Element' + - type: object + properties: + reference: + type: string + type: + type: string + identifier: + $ref: '#/components/schemas/Identifier' + display: + type: string + Element: + type: object + properties: + id: + type: string + element: + type: integer + format: int64 + Patient: + type: object + properties: + id: + type: string + ref: + $ref: '#/components/schemas/Reference' \ No newline at end of file diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/record/typeref_records_with_interdependency.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/record/typeref_records_with_interdependency.yaml new file mode 100644 index 000000000..863c90162 --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/record/typeref_records_with_interdependency.yaml @@ -0,0 +1,95 @@ +openapi: 3.0.1 +info: + title: PayloadV + version: 0.0.0 +servers: + - url: "{server}:{port}/payloadV" + variables: + server: + default: http://localhost + port: + default: "9090" +paths: + /fhir/r4/Patient/{id}: + get: + operationId: getFhirR4PatientId + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Ok + content: + application/fhir+json: + schema: + $ref: '#/components/schemas/Patient' + application/fhir+xml: + schema: + $ref: '#/components/schemas/Patient' +components: + schemas: + Extension: + $ref: '#/components/schemas/ExtensionExtension' + Identifier: + allOf: + - $ref: '#/components/schemas/Element' + - type: object + properties: + value: + type: string + assigner: + $ref: '#/components/schemas/Reference' + id: + type: string + element: + type: integer + format: int64 + Reference: + allOf: + - $ref: '#/components/schemas/Element' + - type: object + properties: + reference: + type: string + type: + type: string + identifier: + $ref: '#/components/schemas/Identifier' + display: + type: string + id: + type: string + element: + type: integer + format: int64 + Element: + type: object + properties: + id: + type: string + element: + type: integer + format: int64 + ExtensionExtension: + allOf: + - $ref: '#/components/schemas/Element' + - type: object + properties: + extension: + type: array + items: + $ref: '#/components/schemas/Extension' + Patient: + type: object + properties: + ext: + type: array + items: + $ref: '#/components/schemas/Extension' + id: + type: string + ref: + $ref: '#/components/schemas/Reference' diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/record/union_records_with_interdependency.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/record/union_records_with_interdependency.yaml new file mode 100644 index 000000000..41699a1ff --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/record/union_records_with_interdependency.yaml @@ -0,0 +1,165 @@ +openapi: 3.0.1 +info: + title: PayloadV + version: 0.0.0 +servers: + - url: "{server}:{port}/payloadV" + variables: + server: + default: http://localhost + port: + default: "9090" +paths: + /fhir/r4/Patient/{id}: + get: + operationId: getFhirR4PatientId + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Ok + content: + application/fhir+json: + schema: + $ref: '#/components/schemas/Patient' + application/fhir+xml: + schema: + $ref: '#/components/schemas/Patient' +components: + schemas: + Extension: + oneOf: + - $ref: '#/components/schemas/ExtensionExtension' + - $ref: '#/components/schemas/StringExtension' + - $ref: '#/components/schemas/CodingExtension' + - $ref: '#/components/schemas/CodeExtension' + - $ref: '#/components/schemas/IntegerExtension' + CodeExtension: + required: + - url + - valueCode + type: object + properties: + url: + $ref: '#/components/schemas/uri' + valueCode: + type: string + Identifier: + allOf: + - $ref: '#/components/schemas/Element' + - type: object + properties: + value: + type: string + assigner: + $ref: '#/components/schemas/Reference' + id: + type: string + element: + type: integer + format: int64 + Coding: + allOf: + - $ref: '#/components/schemas/Element' + - type: object + properties: + id: + type: string + extension: + type: array + items: + $ref: '#/components/schemas/Extension' + system: + $ref: '#/components/schemas/uri' + version: + type: string + display: + type: string + userSelected: + type: boolean + element: + type: integer + format: int64 + CodingExtension: + required: + - url + - valueCoding + type: object + properties: + url: + $ref: '#/components/schemas/uri' + valueCoding: + $ref: '#/components/schemas/Coding' + Reference: + allOf: + - $ref: '#/components/schemas/Element' + - type: object + properties: + reference: + type: string + type: + type: string + identifier: + $ref: '#/components/schemas/Identifier' + display: + type: string + id: + type: string + element: + type: integer + format: int64 + Element: + type: object + properties: + id: + type: string + element: + type: integer + format: int64 + StringExtension: + required: + - url + - valueString + type: object + properties: + url: + $ref: '#/components/schemas/uri' + valueString: + type: string + ExtensionExtension: + allOf: + - $ref: '#/components/schemas/Element' + - type: object + properties: + extension: + type: array + items: + $ref: '#/components/schemas/Extension' + IntegerExtension: + required: + - url + - valueInteger + type: object + properties: + url: + $ref: '#/components/schemas/uri' + valueInteger: + type: integer + format: int64 + Patient: + type: object + properties: + ext: + type: array + items: + $ref: '#/components/schemas/Extension' + id: + type: string + ref: + $ref: '#/components/schemas/Reference' + uri: + type: string diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/record/cyclic_record_with_typeInclusion.bal b/openapi-cli/src/test/resources/ballerina-to-openapi/record/cyclic_record_with_typeInclusion.bal new file mode 100644 index 000000000..903c1bae3 --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/record/cyclic_record_with_typeInclusion.bal @@ -0,0 +1,40 @@ +import ballerina/http; + +type Patient record {| + string id?; + Reference ref?; +|}; + +public type Reference record {| + *Element; + string reference?; + string 'type?; + Identifier identifier?; + string display?; +|}; + +public type Identifier record {| + *Element; + string value?; + Reference assigner?; +|}; + +public type Element record {| + string id?; + int element?; +|}; + + +service /payloadV on new http:Listener(9090) { + + // Read the current state of the resource represented by the given id. + isolated resource function get fhir/r4/Patient/[string id]() + returns @http:Payload {mediaType: ["application/fhir+json", "application/fhir+xml"]} + Patient { + Patient patient = { + id: id + }; + + return patient; + } +} diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/record/typeref_records_with_interdependency.bal b/openapi-cli/src/test/resources/ballerina-to-openapi/record/typeref_records_with_interdependency.bal new file mode 100644 index 000000000..b15613281 --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/record/typeref_records_with_interdependency.bal @@ -0,0 +1,49 @@ +import ballerina/http; + + +type Patient record {| + Extension[] ext?; + string id?; + Reference ref?; +|}; + +public type Extension ExtensionExtension; + +public type ExtensionExtension record {| + *Element; + Extension[] extension?; +|}; + +public type Reference record {| + *Element; + string reference?; + string 'type?; + Identifier identifier?; + string display?; +|}; + +public type Identifier record {| + *Element; + string value?; + Reference assigner?; +|}; + +public type Element record {| + string id?; + int element?; +|}; + + +service /payloadV on new http:Listener(9090) { + + // Read the current state of the resource represented by the given id. + isolated resource function get fhir/r4/Patient/[string id]() + returns @http:Payload {mediaType: ["application/fhir+json", "application/fhir+xml"]} + Patient { + Patient patient = { + id: id + }; + + return patient; + } +} diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/record/union_records_with_interdependency.bal b/openapi-cli/src/test/resources/ballerina-to-openapi/record/union_records_with_interdependency.bal new file mode 100644 index 000000000..7724d251d --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/record/union_records_with_interdependency.bal @@ -0,0 +1,82 @@ +import ballerina/http; + + +type Patient record {| + Extension[] ext?; + string id?; + Reference ref?; +|}; + +public type Extension ExtensionExtension|StringExtension|CodingExtension|CodeExtension|IntegerExtension; + +public type ExtensionExtension record {| + *Element; + Extension[] extension?; +|}; + +public type StringExtension record {| + uri url; + string valueString; +|}; + +public type uri string; +public type CodingExtension record {| + uri url; + Coding valueCoding; +|}; + +public type Coding record {| + *Element; + + string id?; + Extension[] extension?; + uri system?; + string 'version?; + string display?; + boolean userSelected?; +|}; + +public type CodeExtension record {| + uri url; + string valueCode; +|}; + +public type IntegerExtension record {| + uri url; + int valueInteger; +|}; + + +public type Reference record {| + *Element; + string reference?; + string 'type?; + Identifier identifier?; + string display?; +|}; + +public type Identifier record {| + *Element; + string value?; + Reference assigner?; +|}; + +public type Element record {| + string id?; + int element?; +|}; + + +service /payloadV on new http:Listener(9090) { + + // Read the current state of the resource represented by the given id. + isolated resource function get fhir/r4/Patient/[string id]() + returns @http:Payload {mediaType: ["application/fhir+json", "application/fhir+xml"]} + Patient { + Patient patient = { + id: id + }; + + return patient; + } +}