diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/parameter/AbstractParameterMapper.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/parameter/AbstractParameterMapper.java index ab5fcb092..7368345e4 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/parameter/AbstractParameterMapper.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/parameter/AbstractParameterMapper.java @@ -18,6 +18,7 @@ package io.ballerina.openapi.service.mapper.parameter; import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.Symbol; import io.ballerina.compiler.api.symbols.TypeSymbol; import io.ballerina.compiler.syntax.tree.DefaultableParameterNode; import io.ballerina.compiler.syntax.tree.Node; @@ -27,6 +28,9 @@ import java.util.List; import java.util.Objects; +import java.util.Optional; + +import static io.ballerina.openapi.service.mapper.utils.MapperCommonUtils.getConstantValues; /** * This {@link AbstractParameterMapper} class represents the abstract parameter mapper. @@ -55,8 +59,13 @@ public void setParameter() throws ParameterMapperException { parameterList.forEach(operationInventory::setParameter); } - static Object getDefaultValue(DefaultableParameterNode parameterNode) { + static Object getDefaultValue(DefaultableParameterNode parameterNode, SemanticModel semanticModel) { Node defaultValueExpression = parameterNode.expression(); + Optional symbol = semanticModel.symbol(defaultValueExpression); + Optional constantValues = getConstantValues(symbol); + if (constantValues.isPresent()) { + return constantValues.get(); + } if (MapperCommonUtils.isNotSimpleValueLiteralKind(defaultValueExpression.kind())) { return null; } diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/parameter/HeaderParameterMapper.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/parameter/HeaderParameterMapper.java index 80608c3dc..6763564f0 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/parameter/HeaderParameterMapper.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/parameter/HeaderParameterMapper.java @@ -73,7 +73,8 @@ public HeaderParameterMapper(ParameterNode parameterNode, Map ap this.description = apiDocs.get(headerParameter.getName().get()); this.treatNilableAsOptional = treatNilableAsOptional; if (parameterNode instanceof DefaultableParameterNode defaultableHeaderParam) { - this.defaultValue = AbstractParameterMapper.getDefaultValue(defaultableHeaderParam); + this.defaultValue = AbstractParameterMapper.getDefaultValue(defaultableHeaderParam, + additionalData.semanticModel()); } this.typeMapper = typeMapper; } diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/parameter/QueryParameterMapper.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/parameter/QueryParameterMapper.java index 36a7b34b2..6369f4090 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/parameter/QueryParameterMapper.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/parameter/QueryParameterMapper.java @@ -69,7 +69,8 @@ public QueryParameterMapper(ParameterNode parameterNode, Map api this.semanticModel = additionalData.semanticModel(); this.typeMapper = typeMapper; if (parameterNode instanceof DefaultableParameterNode defaultableQueryParam) { - this.defaultValue = AbstractParameterMapper.getDefaultValue(defaultableQueryParam); + this.defaultValue = AbstractParameterMapper.getDefaultValue(defaultableQueryParam, + additionalData.semanticModel()); } } } diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/RecordTypeMapper.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/RecordTypeMapper.java index 04db223f9..8b6eb862d 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/RecordTypeMapper.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/RecordTypeMapper.java @@ -17,9 +17,11 @@ */ package io.ballerina.openapi.service.mapper.type; +import io.ballerina.compiler.api.SemanticModel; import io.ballerina.compiler.api.symbols.IntersectionTypeSymbol; import io.ballerina.compiler.api.symbols.RecordFieldSymbol; import io.ballerina.compiler.api.symbols.RecordTypeSymbol; +import io.ballerina.compiler.api.symbols.Symbol; import io.ballerina.compiler.api.symbols.TypeDescKind; import io.ballerina.compiler.api.symbols.TypeReferenceTypeSymbol; import io.ballerina.compiler.api.symbols.TypeSymbol; @@ -48,6 +50,7 @@ import java.util.Optional; import java.util.Set; +import static io.ballerina.openapi.service.mapper.utils.MapperCommonUtils.getConstantValues; import static io.ballerina.openapi.service.mapper.utils.MapperCommonUtils.getRecordFieldTypeDescription; import static io.ballerina.openapi.service.mapper.utils.MapperCommonUtils.getTypeName; @@ -58,7 +61,6 @@ * @since 1.9.0 */ public class RecordTypeMapper extends AbstractTypeMapper { - public RecordTypeMapper(TypeReferenceTypeSymbol typeSymbol, AdditionalData additionalData) { super(typeSymbol, additionalData); } @@ -71,14 +73,19 @@ public Schema getReferenceSchema(Components components) { public static Schema getSchema(RecordTypeSymbol typeSymbol, Components components, String recordName, AdditionalData additionalData) { + Set fieldsOnlyForRequiredList = new HashSet<>(); ObjectSchema schema = new ObjectSchema(); Set requiredFields = new HashSet<>(); Map recordFieldMap = new LinkedHashMap<>(typeSymbol.fieldDescriptors()); - List allOfSchemaList = mapIncludedRecords(typeSymbol, components, recordFieldMap, additionalData); + List allOfSchemaList = mapIncludedRecords(typeSymbol, components, recordFieldMap, additionalData, + recordName, fieldsOnlyForRequiredList); + + RecordFieldMappingContext mappingContext = new RecordFieldMappingContext( + recordFieldMap, components, requiredFields, recordName, false, additionalData, + fieldsOnlyForRequiredList); - Map properties = mapRecordFields(recordFieldMap, components, requiredFields, - recordName, false, additionalData); + Map properties = mapRecordFields(mappingContext); Optional restFieldType = typeSymbol.restTypeDescriptor(); if (restFieldType.isPresent()) { @@ -90,8 +97,8 @@ public static Schema getSchema(RecordTypeSymbol typeSymbol, Components component schema.additionalProperties(false); } - schema.setProperties(properties); schema.setRequired(requiredFields.stream().toList()); + schema.setProperties(properties); if (!allOfSchemaList.isEmpty()) { ObjectSchema schemaWithAllOf = new ObjectSchema(); allOfSchemaList.add(schema); @@ -103,7 +110,8 @@ public static Schema getSchema(RecordTypeSymbol typeSymbol, Components component static List mapIncludedRecords(RecordTypeSymbol typeSymbol, Components components, Map recordFieldMap, - AdditionalData additionalData) { + AdditionalData additionalData, String recordName, + Set fieldsOnlyForRequiredList) { List allOfSchemaList = new ArrayList<>(); List typeInclusions = typeSymbol.typeInclusions(); for (TypeSymbol typeInclusion : typeInclusions) { @@ -120,18 +128,146 @@ static List mapIncludedRecords(RecordTypeSymbol typeSymbol, Components c .typeDescriptor(); Map includedRecordFieldMap = includedRecordTypeSymbol.fieldDescriptors(); for (Map.Entry includedRecordField : includedRecordFieldMap.entrySet()) { - recordFieldMap.remove(includedRecordField.getKey()); + if (!recordFieldMap.containsKey(includedRecordField.getKey())) { + continue; + } + RecordFieldSymbol recordFieldSymbol = recordFieldMap.get(includedRecordField.getKey()); + RecordFieldSymbol includedRecordFieldValue = includedRecordField.getValue(); + + if (!includedRecordFieldValue.typeDescriptor().equals(recordFieldSymbol.typeDescriptor())) { + continue; + } + IncludedFieldContext context = new IncludedFieldContext(recordFieldMap, recordName, + typeInclusion, includedRecordField, recordFieldSymbol, includedRecordFieldValue + ); + eliminateRedundantFields(context, additionalData, fieldsOnlyForRequiredList); } } } return allOfSchemaList; } - public static Map mapRecordFields(Map recordFieldMap, - Components components, Set requiredFields, - String recordName, boolean treatNilableAsOptional, - AdditionalData additionalData) { + private static void eliminateRedundantFields(IncludedFieldContext context, AdditionalData additionalData, + Set fieldsOnlyForRequiredList) { + Map recordFieldMap = context.recordFieldMap(); + String recordName = context.recordName(); + TypeSymbol typeInclusion = context.typeInclusion(); + Map.Entry includedRecordField = context.includedRecordField(); + RecordFieldSymbol recordFieldSymbol = context.recordFieldSymbol(); + RecordFieldSymbol includedRecordFieldValue = context.includedRecordFieldValue(); + + boolean recordHasDefault = recordFieldSymbol.hasDefaultValue(); + boolean includedHasDefault = includedRecordFieldValue.hasDefaultValue(); + boolean hasTypeInclusionName = typeInclusion.getName().isPresent(); + boolean isIncludedOptional = includedRecordFieldValue.isOptional(); + boolean isRecordFieldOptional = recordFieldSymbol.isOptional(); + boolean recordFieldName = recordFieldSymbol.getName().isPresent(); + + if (recordHasDefault && includedHasDefault && hasTypeInclusionName) { + Optional recordFieldDefaultValueOpt = getRecordFieldDefaultValue(recordName, + includedRecordField.getKey(), additionalData.moduleMemberVisitor(), + additionalData.semanticModel()); + + Optional includedFieldDefaultValueOpt = getRecordFieldDefaultValue( + typeInclusion.getName().get(), includedRecordField.getKey(), + additionalData.moduleMemberVisitor(), additionalData.semanticModel()); + + /* + This check the scenarios + ex: + type RecA record {| + string a = "a"; + string aa; + |}; + type RecD record {| + *RecA; + string a = "aad"; + int d; + |}; + */ + boolean defaultsAreEqual = recordFieldDefaultValueOpt.isPresent() + && includedFieldDefaultValueOpt.isPresent() + && recordFieldDefaultValueOpt.get().toString() + .equals(includedFieldDefaultValueOpt.get().toString()); + + /* + This checks the scenario where RecA has `a` defaultable field. In this case, the + .hasDefaultValue() API returns true for both records, but RecA provides the value of the default. + ex: + type RecA record {| + string a = "a"; + string aa; + |}; + type RecB record {| + *RecA; + int b; + |}; + */ + boolean onlyIncludedHasDefault = recordFieldDefaultValueOpt.isEmpty() && + includedFieldDefaultValueOpt.isPresent(); + + if (defaultsAreEqual || onlyIncludedHasDefault) { + recordFieldMap.remove(includedRecordField.getKey()); + } + } else if (!recordHasDefault && !includedHasDefault) { + recordFieldMap.remove(includedRecordField.getKey()); + } + if (!isRecordFieldOptional && isIncludedOptional && !recordHasDefault && recordFieldName) { + fieldsOnlyForRequiredList.add(MapperCommonUtils.unescapeIdentifier(recordFieldSymbol.getName().get())); + recordFieldMap.remove(includedRecordField.getKey()); + } + } + + /** + * Encapsulates the context of included fields in a record for processing. + * + * @param recordFieldMap A map containing record field symbols. + * @param recordName The name of the record being processed. + * @param typeInclusion The type symbol representing type inclusions in the record. + * @param includedRecordField An entry representing the included record field and its symbol. + * @param recordFieldSymbol The symbol of the current record field being processed. + * @param includedRecordFieldValue The symbol of the field in the included record. + */ + public record IncludedFieldContext( + Map recordFieldMap, + String recordName, + TypeSymbol typeInclusion, + Map.Entry includedRecordField, + RecordFieldSymbol recordFieldSymbol, + RecordFieldSymbol includedRecordFieldValue) { + } + + /** + * Encapsulates the context needed for mapping record fields to schemas. + * + * @param recordFieldMap A map containing record field symbols. + * @param components Components used for managing and storing schemas during mapping. + * @param requiredFields A set of field names that are required in the mapped schema. + * @param recordName The name of the record being processed. + * @param treatNilableAsOptional Flag indicating whether nilable fields should be treated as optional. + * @param additionalData Additional data required for schema generation and field processing. + * @param fieldsOnlyForRequiredList A set of fields that should be exclusively marked as required. + */ + public record RecordFieldMappingContext( + Map recordFieldMap, + Components components, + Set requiredFields, + String recordName, + boolean treatNilableAsOptional, + AdditionalData additionalData, + Set fieldsOnlyForRequiredList) { + } + + public static Map mapRecordFields(RecordFieldMappingContext context) { + Map recordFieldMap = context.recordFieldMap(); + Components components = context.components(); + Set requiredFields = context.requiredFields(); + String recordName = context.recordName(); + boolean treatNilableAsOptional = context.treatNilableAsOptional(); + AdditionalData additionalData = context.additionalData(); + Set fieldsOnlyForRequiredList = context.fieldsOnlyForRequiredList(); Map properties = new LinkedHashMap<>(); + for (Map.Entry recordField : recordFieldMap.entrySet()) { RecordFieldSymbol recordFieldSymbol = recordField.getValue(); String recordFieldName = MapperCommonUtils.unescapeIdentifier(recordField.getKey().trim()); @@ -139,6 +275,9 @@ public static Map mapRecordFields(Map (!treatNilableAsOptional || !UnionTypeMapper.hasNilableType(recordFieldSymbol.typeDescriptor()))) { requiredFields.add(recordFieldName); } + if (!fieldsOnlyForRequiredList.isEmpty()) { + requiredFields.addAll(fieldsOnlyForRequiredList); + } String recordFieldDescription = getRecordFieldTypeDescription(recordFieldSymbol); Schema recordFieldSchema = TypeMapperImpl.getTypeSchema(recordFieldSymbol.typeDescriptor(), components, additionalData); @@ -147,7 +286,7 @@ public static Map mapRecordFields(Map } if (recordFieldSymbol.hasDefaultValue()) { Optional recordFieldDefaultValueOpt = getRecordFieldDefaultValue(recordName, recordFieldName, - additionalData.moduleMemberVisitor()); + additionalData.moduleMemberVisitor(), additionalData.semanticModel()); if (recordFieldDefaultValueOpt.isPresent()) { TypeMapper.setDefaultValue(recordFieldSchema, recordFieldDefaultValueOpt.get()); } else { @@ -162,17 +301,19 @@ public static Map mapRecordFields(Map } public static Optional getRecordFieldDefaultValue(String recordName, String fieldName, - ModuleMemberVisitor moduleMemberVisitor) { + ModuleMemberVisitor moduleMemberVisitor, + SemanticModel semanticModel) { Optional recordDefNodeOpt = moduleMemberVisitor.getTypeDefinitionNode(recordName); if (recordDefNodeOpt.isPresent() && recordDefNodeOpt.get().typeDescriptor() instanceof RecordTypeDescriptorNode recordDefNode) { - return getRecordFieldDefaultValue(fieldName, recordDefNode); + return getRecordFieldDefaultValue(fieldName, recordDefNode, semanticModel); } return Optional.empty(); } private static Optional getRecordFieldDefaultValue(String fieldName, - RecordTypeDescriptorNode recordDefNode) { + RecordTypeDescriptorNode recordDefNode, + SemanticModel semanticModel) { NodeList recordFields = recordDefNode.fields(); RecordFieldWithDefaultValueNode defaultValueNode = recordFields.stream() .filter(field -> field instanceof RecordFieldWithDefaultValueNode) @@ -183,6 +324,11 @@ private static Optional getRecordFieldDefaultValue(String fieldName, return Optional.empty(); } ExpressionNode defaultValueExpression = defaultValueNode.expression(); + Optional symbol = semanticModel.symbol(defaultValueExpression); + Optional value = getConstantValues(symbol); + if (value.isPresent()) { + return value; + } if (MapperCommonUtils.isNotSimpleValueLiteralKind(defaultValueExpression.kind())) { return Optional.empty(); } diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/TypeMapperImpl.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/TypeMapperImpl.java index f22a780f2..943e0b812 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/TypeMapperImpl.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/type/TypeMapperImpl.java @@ -32,6 +32,7 @@ import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.media.Schema; +import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -113,8 +114,10 @@ protected static void createComponentMapping(TypeReferenceTypeSymbol typeSymbol, public Map getSchemaForRecordFields(Map recordFieldMap, Set requiredFields, String recordName, boolean treatNilableAsOptional) { - return RecordTypeMapper.mapRecordFields(recordFieldMap, components, requiredFields, recordName, - treatNilableAsOptional, componentMapperData); + RecordTypeMapper.RecordFieldMappingContext context = new RecordTypeMapper.RecordFieldMappingContext( + recordFieldMap, components, requiredFields, recordName, treatNilableAsOptional, componentMapperData, + new HashSet<>()); + return RecordTypeMapper.mapRecordFields(context); } public TypeSymbol getReferredType(TypeSymbol typeSymbol) { diff --git a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/utils/MapperCommonUtils.java b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/utils/MapperCommonUtils.java index 808f48354..aa33462e1 100644 --- a/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/utils/MapperCommonUtils.java +++ b/ballerina-to-openapi/src/main/java/io/ballerina/openapi/service/mapper/utils/MapperCommonUtils.java @@ -34,6 +34,7 @@ import io.ballerina.compiler.api.symbols.TypeReferenceTypeSymbol; import io.ballerina.compiler.api.symbols.TypeSymbol; import io.ballerina.compiler.api.symbols.UnionTypeSymbol; +import io.ballerina.compiler.api.values.ConstantValue; import io.ballerina.compiler.syntax.tree.AnnotationNode; import io.ballerina.compiler.syntax.tree.BasicLiteralNode; import io.ballerina.compiler.syntax.tree.DefaultableParameterNode; @@ -555,6 +556,16 @@ public static Optional getResourceFunction(Node function) { return Optional.empty(); } + public static Optional getConstantValues(Optional symbol) { + if (symbol.isPresent() && symbol.get() instanceof ConstantSymbol constantSymbol) { + Object constValue = constantSymbol.constValue(); + if (constValue instanceof ConstantValue value) { + return Optional.of(value.value()); + } + } + return Optional.empty(); + } + public static Node getTypeDescriptor(TypeDefinitionNode typeDefinitionNode) { Node node = typeDefinitionNode.typeDescriptor(); if (node instanceof DistinctTypeDescriptorNode distinctTypeDescriptorNode) { 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 b08d12be8..096634ffa 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 @@ -125,6 +125,12 @@ public void testRestFieldInRecord() throws IOException { TestUtils.compareWithGeneratedFile(ballerinaFilePath, "record/record_rest_param.yaml"); } + @Test(description = "Test for record has included record with same fields") + public void testIncludedRecordWithSameFields() throws IOException { + Path ballerinaFilePath = RES_DIR.resolve("record/included_record.bal"); + TestUtils.compareWithGeneratedFile(ballerinaFilePath, "record/included_record.yaml"); + } + @AfterMethod public void cleanUp() { TestUtils.deleteDirectory(this.tempDir); diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/generators/schema/AdvanceRecordTypeTests.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/schema/AdvanceRecordTypeTests.java index 108d776fe..db9322bf2 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/generators/schema/AdvanceRecordTypeTests.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/schema/AdvanceRecordTypeTests.java @@ -108,4 +108,22 @@ public void generateForSchemaHasObjectTypeOnly() throws IOException, BallerinaOp serviceGenerationHandler.generateServiceFiles(oasServiceMetadata); syntaxTree = TypeHandler.getInstance().generateTypeSyntaxTree(); } + + @Test(description = "Generate record for schema has allOf types ") + public void generateTypeForTypeInclusions() throws IOException, BallerinaOpenApiException, + FormatterException { + Path definitionPath = RES_DIR.resolve("swagger/inclusion_types.yaml"); + OpenAPI openAPI = GeneratorUtils.normalizeOpenAPI(definitionPath, true, false); + TypeHandler.createInstance(openAPI, false); + ServiceGenerationHandler serviceGenerationHandler = new ServiceGenerationHandler(); + OASServiceMetadata oasServiceMetadata = new OASServiceMetadata.Builder() + .withOpenAPI(openAPI) + .withNullable(false) + .withFilters(FILTER) + .build(); + serviceGenerationHandler.generateServiceFiles(oasServiceMetadata); + syntaxTree = TypeHandler.getInstance().generateTypeSyntaxTree(); + assertGeneratedSyntaxTreeContainsExpectedSyntaxTree("schema/ballerina/type_inclusion.bal", + syntaxTree); + } } diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/header_scenario13.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/header_scenario13.yaml index a960053ef..87d2d8702 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/header_scenario13.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/header_scenario13.yaml @@ -134,6 +134,28 @@ paths: - 1 - 2 - 3 + - name: h8 + in: header + schema: + type: string + default: Pod + - name: header31 + in: header + schema: + type: string + default: Pod + - name: header32 + in: header + required: true + schema: + type: array + items: + type: integer + format: int64 + default: + - 1 + - 2 + - 3 responses: "200": description: Ok diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/parameter_annotation/annotated_query.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/parameter_annotation/annotated_query.yaml index ce2090314..f8c1dca86 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/parameter_annotation/annotated_query.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/parameter_annotation/annotated_query.yaml @@ -104,6 +104,51 @@ paths: application/json: schema: $ref: "#/components/schemas/ErrorPayload" + /student12: + post: + operationId: postStudent12 + parameters: + - name: status + in: query + schema: + type: string + default: Service + responses: + "201": + description: Created + content: + application/json: + schema: + type: object + "400": + description: BadRequest + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorPayload" + /student13: + post: + operationId: postStudent13 + parameters: + - name: kind + in: query + schema: + allOf: + - $ref: "#/components/schemas/ResourceKind" + default: Service + responses: + "201": + description: Created + content: + application/json: + schema: + type: object + "400": + description: BadRequest + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorPayload" components: schemas: ErrorPayload: @@ -129,6 +174,11 @@ components: type: string method: type: string + ResourceKind: + type: string + enum: + - Service + - Pod Status: type: string enum: diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/record/included_record.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/record/included_record.yaml new file mode 100644 index 000000000..219d84805 --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/record/included_record.yaml @@ -0,0 +1,357 @@ +openapi: 3.0.1 +info: + title: PayloadV + version: 0.0.0 +servers: + - url: "{server}:{port}/payloadV" + variables: + server: + default: http://localhost + port: + default: "7080" +paths: + /pods: + get: + operationId: getPods + responses: + "200": + description: Ok + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Pod" + /services: + get: + operationId: getServices + responses: + "200": + description: Ok + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Service" + /recB: + get: + operationId: getRecb + responses: + "200": + description: Ok + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RecB" + /recC: + get: + operationId: getRecc + responses: + "200": + description: Ok + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RecC" + /recD: + post: + operationId: postRecd + responses: + "201": + description: Created + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RecD" + /recE: + post: + operationId: postRece + responses: + "201": + description: Created + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RecE" + /recH: + post: + operationId: postRech + responses: + "201": + description: Created + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RecH" + /recI: + post: + operationId: postReci + responses: + "201": + description: Created + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RecI" + /recJ: + post: + operationId: postRecj + responses: + "202": + description: Accepted + content: + application/json: + schema: + $ref: "#/components/schemas/RecK" +components: + schemas: + Metadata: + required: + - name + type: object + properties: + name: + type: string + displayName: + type: string + description: + type: string + Pod: + type: object + allOf: + - $ref: "#/components/schemas/Resource" + - required: + - spec + type: object + properties: + kind: + allOf: + - $ref: "#/components/schemas/ResourceKind" + default: Pod + spec: + $ref: "#/components/schemas/PodSpec" + PodSpec: + required: + - nodeName + type: object + properties: + nodeName: + type: string + RecA: + required: + - aa + type: object + properties: + a: + type: string + default: a + aa: + type: string + additionalProperties: false + RecB: + type: object + allOf: + - $ref: "#/components/schemas/RecA" + - required: + - b + type: object + properties: + b: + type: integer + format: int64 + additionalProperties: false + RecC: + type: object + allOf: + - $ref: "#/components/schemas/RecA" + - required: + - c + type: object + properties: + aa: + type: string + default: aa + c: + type: integer + format: int64 + additionalProperties: false + RecD: + type: object + allOf: + - $ref: "#/components/schemas/RecA" + - required: + - d + type: object + properties: + a: + type: string + default: aad + d: + type: integer + format: int64 + additionalProperties: false + RecE: + type: object + allOf: + - $ref: "#/components/schemas/RecA" + - required: + - a + - e + type: object + properties: + a: + type: string + e: + type: string + additionalProperties: false + RecF: + type: object + properties: + f: + type: integer + format: int64 + additionalProperties: false + RecG: + type: object + allOf: + - $ref: "#/components/schemas/RecF" + - type: object + properties: + g: + type: integer + format: int64 + additionalProperties: false + RecH: + type: object + allOf: + - $ref: "#/components/schemas/RecG" + - required: + - f + - g + - h + type: object + properties: + h: + type: string + additionalProperties: false + RecI: + type: object + allOf: + - $ref: "#/components/schemas/RecG" + - required: + - g + - i + type: object + properties: + f: + type: integer + format: int64 + default: 10 + i: + type: string + additionalProperties: false + RecK: + type: object + allOf: + - $ref: "#/components/schemas/RecA" + - $ref: "#/components/schemas/RecF" + - $ref: "#/components/schemas/RecL" + - required: + - a + - first-name + - k + type: object + properties: + k: + type: integer + format: int64 + a: + type: string + additionalProperties: false + RecL: + required: + - id + type: object + properties: + first-name: + type: string + id: + type: integer + format: int64 + additionalProperties: false + Resource: + type: object + allOf: + - $ref: "#/components/schemas/ResourceBase" + - required: + - spec + type: object + properties: + spec: + type: object + properties: {} + status: + $ref: "#/components/schemas/Status" + ResourceBase: + required: + - group + - kind + - metadata + - version + type: object + properties: + group: + type: string + version: + type: string + kind: + $ref: "#/components/schemas/ResourceKind" + metadata: + $ref: "#/components/schemas/Metadata" + ResourceKind: + type: string + enum: + - Service + - Pod + Service: + type: object + allOf: + - $ref: "#/components/schemas/Resource" + - required: + - spec + type: object + properties: + kind: + allOf: + - $ref: "#/components/schemas/ResourceKind" + default: Service + spec: + $ref: "#/components/schemas/ServiceSpec" + ServiceSpec: + required: + - clusterIP + type: object + properties: + clusterIP: + type: string + Status: + required: + - observedGeneration + type: object + properties: + observedGeneration: + type: integer + format: int64 diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/headers/header_scenario13.bal b/openapi-cli/src/test/resources/ballerina-to-openapi/headers/header_scenario13.bal index 71d708744..0532e54f3 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/headers/header_scenario13.bal +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/headers/header_scenario13.bal @@ -21,13 +21,22 @@ type Header2 record {| boolean header24; |}; +type Header3 record {| + string header31 = RESOURCE_KIND_POD; + int[] header32; +|}; + +public const RESOURCE_KIND_POD = "Pod"; + service /payloadV on helloEp { resource function get header(@http:Header Header1 h, @http:Header string h0 = "", @http:Header string h1 = "\"John\"", @http:Header string[] h2 = [], @http:Header string[] h3 = ["one", "two", "three"], @http:Header int[] h4 = [1, 2, 3], @http:Header float[] h5 = [1, 2.3, 4.56], @http:Header Record h6 = {name: "John", city: "London"}, - @http:Header Header2 h7= {header21: "header1", header22: [1,2,3], header24: false, header23: []}) returns string { + @http:Header Header2 h7= {header21: "header1", header22: [1,2,3], header24: false, header23: []}, + @http:Header string h8 = RESOURCE_KIND_POD, + @http:Header Header3 h9= {header32: [1,2,3]}) returns string { return "new"; } } diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/parameter_annotation/annotated_query.bal b/openapi-cli/src/test/resources/ballerina-to-openapi/parameter_annotation/annotated_query.bal index 8851a1252..203496cf8 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/parameter_annotation/annotated_query.bal +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/parameter_annotation/annotated_query.bal @@ -2,6 +2,11 @@ import ballerina/http; listener http:Listener ep0 = new (443, config = {host: "petstore3.swagger.io"}); +public const RESOURCE_KIND_SERVICE = "Service"; +public const RESOURCE_KIND_POD = "Pod"; + +public type ResourceKind RESOURCE_KIND_SERVICE|RESOURCE_KIND_POD; + type Student record { string Name; }; @@ -28,4 +33,12 @@ service /payloadV on ep0 { resource function post student11(@http:Query Status status = "ACTIVE") returns json { return {Name: "john", Status: status}; } + + resource function post student12(@http:Query string status = RESOURCE_KIND_SERVICE) returns json { + return {Name: "john", Status: status}; + } + + resource function post student13(@http:Query ResourceKind kind = RESOURCE_KIND_SERVICE) returns json { + return {Name: "john"}; + } } diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/record/included_record.bal b/openapi-cli/src/test/resources/ballerina-to-openapi/record/included_record.bal new file mode 100644 index 000000000..506280cfb --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/record/included_record.bal @@ -0,0 +1,171 @@ +import ballerina/http; + +public const RESOURCE_KIND_SERVICE = "Service"; +public const RESOURCE_KIND_POD = "Pod"; + +public type ResourceKind RESOURCE_KIND_SERVICE|RESOURCE_KIND_POD; + +public type Metadata record { + string name; + string displayName?; + string description?; +}; + +public type Status record { + int observedGeneration; +}; + +public type ResourceBase record { + string group; + string version; + ResourceKind kind; + Metadata metadata; +}; + +public type Resource record { + *ResourceBase; + record {} spec; + Status status?; +}; + +public type ServiceSpec record { + string clusterIP; +}; + +public type Service record { + *Resource; + ResourceKind kind = RESOURCE_KIND_SERVICE; + ServiceSpec spec; + Status status?; +}; + +public type PodSpec record { + string nodeName; +}; + +public type Pod record { + *Resource; + ResourceKind kind = RESOURCE_KIND_POD; + PodSpec spec; + Status status?; +}; + +type RecA record {| + string a = "a"; + string aa; +|}; + +type RecB record {| + *RecA; + int b; +|}; + +type RecC record {| + *RecA; + string aa = "aa"; + int c; +|}; + +type RecD record {| + *RecA; + string a = "aad"; + int d; +|}; + +// defaultable `a`` makes requried +type RecE record {| + *RecA; + string a; + string e; +|}; + +//optional test cases +type RecF record {| + int f?; +|}; + +type RecG record {| + *RecF; + int g?; +|}; + +type RecH record {| + *RecG; + int f; + int g; + string h; +|}; + +// optional with default value +type RecI record {| + *RecG; + int f = 10; + string i; + int g; +|}; + +type RecJ record {| + *http:Accepted; + RecK body; +|}; + +type RecK record {| + *RecA; + *RecF; + *RecL; + int k; + string a; + string first\-name; +|}; + +type RecL record {| + string first\-name?; + int id; +|}; + +service /payloadV on new http:Listener(7080) { + resource function get pods() returns Pod[] { + return []; + } + + resource function get services() returns Service[] { + return []; + } + + resource function get recB() returns RecB[] { + return []; + } + + resource function get recC() returns RecC[] { + return []; + } + + resource function post recD() returns RecD[] { + return []; + } + + resource function post recE() returns RecE[] { + return []; + } + + resource function post recH() returns RecH[] { + return []; + } + + resource function post recI() returns RecI[] { + return []; + } + + resource function post recJ() returns RecJ { + return { + body: + { + k: 10, + aa: "abc", + a: "abcd", + id: 11, + first\-name: "lnash" + } + }; + } +} diff --git a/openapi-cli/src/test/resources/generators/schema/ballerina/type_inclusion.bal b/openapi-cli/src/test/resources/generators/schema/ballerina/type_inclusion.bal new file mode 100644 index 000000000..93e14cfb6 --- /dev/null +++ b/openapi-cli/src/test/resources/generators/schema/ballerina/type_inclusion.bal @@ -0,0 +1,116 @@ +import ballerina/http; + +public type Status record { + int observedGeneration; +}; + +public type Pod record { + *Resource; + ResourceKind kind = "Pod"; + PodSpec spec; +}; + +public type PodSpec record { + string nodeName; +}; + +public type ResourceBase record { + string group; + string version; + ResourceKind kind; + Metadata metadata; +}; + +public type Metadata record { + string name; + string displayName?; + string description?; +}; + +public type Resource record { + *ResourceBase; + record {} spec; + Status status?; +}; + +public type ServiceSpec record { + string clusterIP; +}; + +public type Service record { + *Resource; + ResourceKind kind = "Service"; + ServiceSpec spec; +}; + +public type RecF record {| + int f?; +|}; + +public type RecG record { + *RecF; + int g?; +}; + +public type RecD record { + *RecA; + string a = "aad"; + int d; +}; + +public type RecE record { + *RecA; + string a; + string e; +}; + +public type RecB record { + *RecA; + int b; +}; + +public type RecC record { + *RecA; + string aa = "aa"; + int c; +}; + +public type RecA record {| + string a = "a"; + string aa; +|}; + +public type RecL record {| + string first\-name?; + int id; +|}; + +public type ResourceKind "Service"|"Pod"; + +public type RecK record { + *RecA; + *RecF; + *RecL; + int k; + string a; + string first\-name; +}; + +public type RecH record { + *RecG; + string h; + int f; + int g; +}; + +public type RecI record { + *RecG; + int f = 10; + string i; + int g; +}; + +public type RecKAccepted record {| + *http:Accepted; + RecK body; +|}; diff --git a/openapi-cli/src/test/resources/generators/schema/swagger/inclusion_types.yaml b/openapi-cli/src/test/resources/generators/schema/swagger/inclusion_types.yaml new file mode 100644 index 000000000..219d84805 --- /dev/null +++ b/openapi-cli/src/test/resources/generators/schema/swagger/inclusion_types.yaml @@ -0,0 +1,357 @@ +openapi: 3.0.1 +info: + title: PayloadV + version: 0.0.0 +servers: + - url: "{server}:{port}/payloadV" + variables: + server: + default: http://localhost + port: + default: "7080" +paths: + /pods: + get: + operationId: getPods + responses: + "200": + description: Ok + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Pod" + /services: + get: + operationId: getServices + responses: + "200": + description: Ok + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Service" + /recB: + get: + operationId: getRecb + responses: + "200": + description: Ok + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RecB" + /recC: + get: + operationId: getRecc + responses: + "200": + description: Ok + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RecC" + /recD: + post: + operationId: postRecd + responses: + "201": + description: Created + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RecD" + /recE: + post: + operationId: postRece + responses: + "201": + description: Created + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RecE" + /recH: + post: + operationId: postRech + responses: + "201": + description: Created + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RecH" + /recI: + post: + operationId: postReci + responses: + "201": + description: Created + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/RecI" + /recJ: + post: + operationId: postRecj + responses: + "202": + description: Accepted + content: + application/json: + schema: + $ref: "#/components/schemas/RecK" +components: + schemas: + Metadata: + required: + - name + type: object + properties: + name: + type: string + displayName: + type: string + description: + type: string + Pod: + type: object + allOf: + - $ref: "#/components/schemas/Resource" + - required: + - spec + type: object + properties: + kind: + allOf: + - $ref: "#/components/schemas/ResourceKind" + default: Pod + spec: + $ref: "#/components/schemas/PodSpec" + PodSpec: + required: + - nodeName + type: object + properties: + nodeName: + type: string + RecA: + required: + - aa + type: object + properties: + a: + type: string + default: a + aa: + type: string + additionalProperties: false + RecB: + type: object + allOf: + - $ref: "#/components/schemas/RecA" + - required: + - b + type: object + properties: + b: + type: integer + format: int64 + additionalProperties: false + RecC: + type: object + allOf: + - $ref: "#/components/schemas/RecA" + - required: + - c + type: object + properties: + aa: + type: string + default: aa + c: + type: integer + format: int64 + additionalProperties: false + RecD: + type: object + allOf: + - $ref: "#/components/schemas/RecA" + - required: + - d + type: object + properties: + a: + type: string + default: aad + d: + type: integer + format: int64 + additionalProperties: false + RecE: + type: object + allOf: + - $ref: "#/components/schemas/RecA" + - required: + - a + - e + type: object + properties: + a: + type: string + e: + type: string + additionalProperties: false + RecF: + type: object + properties: + f: + type: integer + format: int64 + additionalProperties: false + RecG: + type: object + allOf: + - $ref: "#/components/schemas/RecF" + - type: object + properties: + g: + type: integer + format: int64 + additionalProperties: false + RecH: + type: object + allOf: + - $ref: "#/components/schemas/RecG" + - required: + - f + - g + - h + type: object + properties: + h: + type: string + additionalProperties: false + RecI: + type: object + allOf: + - $ref: "#/components/schemas/RecG" + - required: + - g + - i + type: object + properties: + f: + type: integer + format: int64 + default: 10 + i: + type: string + additionalProperties: false + RecK: + type: object + allOf: + - $ref: "#/components/schemas/RecA" + - $ref: "#/components/schemas/RecF" + - $ref: "#/components/schemas/RecL" + - required: + - a + - first-name + - k + type: object + properties: + k: + type: integer + format: int64 + a: + type: string + additionalProperties: false + RecL: + required: + - id + type: object + properties: + first-name: + type: string + id: + type: integer + format: int64 + additionalProperties: false + Resource: + type: object + allOf: + - $ref: "#/components/schemas/ResourceBase" + - required: + - spec + type: object + properties: + spec: + type: object + properties: {} + status: + $ref: "#/components/schemas/Status" + ResourceBase: + required: + - group + - kind + - metadata + - version + type: object + properties: + group: + type: string + version: + type: string + kind: + $ref: "#/components/schemas/ResourceKind" + metadata: + $ref: "#/components/schemas/Metadata" + ResourceKind: + type: string + enum: + - Service + - Pod + Service: + type: object + allOf: + - $ref: "#/components/schemas/Resource" + - required: + - spec + type: object + properties: + kind: + allOf: + - $ref: "#/components/schemas/ResourceKind" + default: Service + spec: + $ref: "#/components/schemas/ServiceSpec" + ServiceSpec: + required: + - clusterIP + type: object + properties: + clusterIP: + type: string + Status: + required: + - observedGeneration + type: object + properties: + observedGeneration: + type: integer + format: int64 diff --git a/openapi-client-native/ballerina-tests/Dependencies.toml b/openapi-client-native/ballerina-tests/Dependencies.toml index a0742b639..d502962cb 100644 --- a/openapi-client-native/ballerina-tests/Dependencies.toml +++ b/openapi-client-native/ballerina-tests/Dependencies.toml @@ -5,7 +5,7 @@ [ballerina] dependencies-toml-version = "2" -distribution-version = "2201.10.0-20240806-083400-aabac46a" +distribution-version = "2201.10.0" [[package]] org = "ballerina" @@ -75,7 +75,7 @@ dependencies = [ [[package]] org = "ballerina" name = "http" -version = "2.12.0" +version = "2.12.2" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "cache"}, @@ -233,7 +233,7 @@ dependencies = [ [[package]] org = "ballerina" name = "mime" -version = "2.10.0" +version = "2.10.1" dependencies = [ {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, diff --git a/openapi-core/src/main/java/io/ballerina/openapi/core/generators/type/generators/AllOfRecordTypeGenerator.java b/openapi-core/src/main/java/io/ballerina/openapi/core/generators/type/generators/AllOfRecordTypeGenerator.java index 94f01f892..020941af4 100644 --- a/openapi-core/src/main/java/io/ballerina/openapi/core/generators/type/generators/AllOfRecordTypeGenerator.java +++ b/openapi-core/src/main/java/io/ballerina/openapi/core/generators/type/generators/AllOfRecordTypeGenerator.java @@ -38,6 +38,7 @@ import io.ballerina.openapi.core.generators.type.model.GeneratorMetaData; import io.ballerina.openapi.core.generators.type.model.RecordMetadata; import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.ComposedSchema; import io.swagger.v3.oas.models.media.Schema; import org.apache.commons.lang3.tuple.ImmutablePair; @@ -67,6 +68,7 @@ import static io.ballerina.compiler.syntax.tree.SyntaxKind.SEMICOLON_TOKEN; import static io.ballerina.compiler.syntax.tree.SyntaxKind.TYPE_KEYWORD; import static io.ballerina.openapi.core.generators.type.diagnostic.TypeGenerationDiagnosticMessages.OAS_TYPE_102; +import static io.ballerina.openapi.core.generators.type.diagnostic.TypeGenerationDiagnosticMessages.OAS_TYPE_103; /** * Generate TypeDefinitionNode and TypeDescriptorNode for allOf schemas. @@ -94,11 +96,13 @@ */ public class AllOfRecordTypeGenerator extends RecordTypeGenerator { private final List> restSchemas = new LinkedList<>(); + private final Map allProperties = new HashMap<>(); public AllOfRecordTypeGenerator(Schema schema, String typeName, boolean ignoreNullableFlag, HashMap subTypesMap, HashMap pregeneratedTypeMap) { super(schema, typeName, ignoreNullableFlag, subTypesMap, pregeneratedTypeMap); + allProperties.putAll(getAllPropertiesFromComposedSchema(schema)); } /** @@ -201,6 +205,7 @@ private ImmutablePair, List>> generateAllOfRecordFields(Lis if (Objects.isNull(required)) { required = new ArrayList<>(); } + updateRFieldsWithRequiredProperties(properties, required); required.addAll(requiredFields); recordFieldList.addAll(addRecordFields(required, properties.entrySet(), typeName)); addAdditionalSchemas(allOfSchema); @@ -230,6 +235,7 @@ private void handleCommonRequiredFields(List requiredFields, Schema refS List required = refSchema.getRequired(); if (Objects.nonNull(required)) { requiredFields.removeAll(required); + updateRFieldsWithRequiredProperties(properties, required); } Set>> fieldProperties = new HashSet<>(); for (Map.Entry> property : properties.entrySet()) { @@ -243,6 +249,14 @@ private void handleCommonRequiredFields(List requiredFields, Schema refS } } + private void updateRFieldsWithRequiredProperties(Map> properties, List required) { + for (String field: required) { + if (!properties.containsKey(field) && allProperties.containsKey(field)) { + properties.put(field, allProperties.get(field)); + } + } + } + private static Map> getPropertiesFromRefSchema(Schema schema) throws InvalidReferenceException { Map> properties = schema.getProperties(); if (Objects.isNull(properties)) { @@ -320,4 +334,48 @@ private void addAdditionalSchemas(Schema refSchema) { restSchemas.add((Schema) refSchema.getAdditionalProperties()); } } + + public Map getAllPropertiesFromComposedSchema(Schema schemaV) { + Map properties = new HashMap<>(); + if (!(schemaV instanceof ComposedSchema composedSchema)) { + return new HashMap<>(); + } + // Process allOf, anyOf, and oneOf schemas, including nested composed schemas + try { + addPropertiesFromSchemas(composedSchema.getAllOf(), properties); + addPropertiesFromSchemas(composedSchema.getAnyOf(), properties); + addPropertiesFromSchemas(composedSchema.getOneOf(), properties); + } catch (InvalidReferenceException e) { + diagnostics.add(new TypeGeneratorDiagnostic(OAS_TYPE_103, e.getMessage())); + } + return properties; + } + + private void addPropertiesFromSchemas(List schemas, Map properties) + throws InvalidReferenceException { + if (schemas != null) { + for (Schema schema : schemas) { + if (schema instanceof ComposedSchema composedSchema) { + // Recursively resolve nested composed schemas + properties.putAll(getAllPropertiesFromComposedSchema(composedSchema)); + } else { + // Add properties from standard schemas or resolved references + properties.putAll(resolveAndGetProperties(schema, GeneratorMetaData.getInstance().getOpenAPI())); + } + } + } + } + + private Map resolveAndGetProperties(Schema schema, OpenAPI openapi) + throws InvalidReferenceException { + if (schema.get$ref() != null) { + String refName; + refName = GeneratorUtils.extractReferenceType(schema.get$ref()); + schema = openapi.getComponents().getSchemas().get(refName); + } + if (schema instanceof ComposedSchema composedSchema) { + return getAllPropertiesFromComposedSchema(composedSchema); + } + return schema != null && schema.getProperties() != null ? schema.getProperties() : new HashMap<>(); + } } diff --git a/openapi-integration-tests/src/test/java/io/ballerina/openapi/cmd/BallerinaToOpenAPITests.java b/openapi-integration-tests/src/test/java/io/ballerina/openapi/cmd/BallerinaToOpenAPITests.java index 83082d548..239f569b5 100644 --- a/openapi-integration-tests/src/test/java/io/ballerina/openapi/cmd/BallerinaToOpenAPITests.java +++ b/openapi-integration-tests/src/test/java/io/ballerina/openapi/cmd/BallerinaToOpenAPITests.java @@ -178,6 +178,30 @@ public void openAPIGenWithBalExt() throws IOException, InterruptedException { "project_openapi_bal_ext/result_1.yaml", true); } + @Test(description = "Map included record type which has same fields") + public void includedRecordTest() throws IOException, InterruptedException { + // 1. generate OAS + executeCommand("project_openapi_with_included_test/service_file.bal", + "payloadV_openapi.yaml", + "project_openapi_with_included_test/result.yaml", false); + // 2. generate type using the OAS which generated point 1 + generateTypes(); + } + + private void generateTypes() throws IOException, InterruptedException { + List buildArgs = new LinkedList<>(); + buildArgs.add("-i"); + buildArgs.add(String.valueOf(TEST_RESOURCE.resolve("payloadV_openapi.yaml"))); + buildArgs.add("-o"); + buildArgs.add(tmpDir.toString()); + boolean successful = TestUtil.executeOpenAPI(DISTRIBUTION_FILE_NAME, TEST_RESOURCE, buildArgs); + Assert.assertTrue(Files.exists(Paths.get(tmpDir.toString()).resolve("types.bal"))); + String generatedOpenAPI = getStringFromGivenBalFile(Paths.get(tmpDir.toString()).resolve("types.bal")); + String expectedYaml = getStringFromGivenBalFile(TEST_RESOURCE.resolve( + "resources/expected_types.bal")); + Assert.assertEquals(expectedYaml, generatedOpenAPI); + } + @AfterClass public void cleanUp() throws IOException { TestUtil.cleanDistribution(); diff --git a/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/result_1.yaml b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/result_1.yaml index 0dafce369..996094380 100644 --- a/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/result_1.yaml +++ b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_bal_ext/result_1.yaml @@ -57,7 +57,7 @@ components: orgName: ballerina pkgName: time moduleName: time - version: 2.4.0 + version: 2.5.0 modulePrefix: time name: Date DateFields: @@ -81,7 +81,7 @@ components: orgName: ballerina pkgName: time moduleName: time - version: 2.4.0 + version: 2.5.0 modulePrefix: time name: DateFields OptionalTimeOfDayFields: @@ -100,7 +100,7 @@ components: orgName: ballerina pkgName: time moduleName: time - version: 2.4.0 + version: 2.5.0 modulePrefix: time name: OptionalTimeOfDayFields Seconds: @@ -111,7 +111,7 @@ components: orgName: ballerina pkgName: time moduleName: time - version: 2.4.0 + version: 2.5.0 modulePrefix: time name: Seconds Student: @@ -165,6 +165,6 @@ components: orgName: ballerina pkgName: time moduleName: time - version: 2.4.0 + version: 2.5.0 modulePrefix: time name: ZoneOffset diff --git a/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_with_included_test/Ballerina.toml b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_with_included_test/Ballerina.toml new file mode 100644 index 000000000..d3cc51d4b --- /dev/null +++ b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_with_included_test/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org= "ballerina" +name= "openapi" +version= "2.0.0" diff --git a/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_with_included_test/result.yaml b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_with_included_test/result.yaml new file mode 100644 index 000000000..18dfb316e --- /dev/null +++ b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_with_included_test/result.yaml @@ -0,0 +1,133 @@ +openapi: 3.0.1 +info: + title: PayloadV + version: 2.0.0 +servers: + - url: "{server}:{port}/payloadV" + variables: + server: + default: http://localhost + port: + default: "7080" +paths: + /Pods: + get: + operationId: getPods + responses: + "200": + description: Ok + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pod' + /Services: + get: + operationId: getServices + responses: + "200": + description: Ok + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Service' +components: + schemas: + Metadata: + required: + - name + type: object + properties: + name: + type: string + displayName: + type: string + description: + type: string + Pod: + type: object + allOf: + - $ref: '#/components/schemas/Resource' + - required: + - spec + type: object + properties: + kind: + allOf: + - $ref: '#/components/schemas/ResourceKind' + default: Pod + spec: + $ref: '#/components/schemas/PodSpec' + PodSpec: + required: + - nodeName + type: object + properties: + nodeName: + type: string + Resource: + type: object + allOf: + - $ref: '#/components/schemas/ResourceBase' + - required: + - spec + type: object + properties: + spec: + type: object + properties: {} + status: + $ref: '#/components/schemas/Status' + ResourceBase: + required: + - group + - kind + - metadata + - version + type: object + properties: + group: + type: string + version: + type: string + kind: + $ref: '#/components/schemas/ResourceKind' + metadata: + $ref: '#/components/schemas/Metadata' + ResourceKind: + type: string + enum: + - Service + - Pod + Service: + type: object + allOf: + - $ref: '#/components/schemas/Resource' + - required: + - spec + type: object + properties: + kind: + allOf: + - $ref: '#/components/schemas/ResourceKind' + default: Service + spec: + $ref: '#/components/schemas/ServiceSpec' + ServiceSpec: + required: + - clusterIP + type: object + properties: + clusterIP: + type: string + Status: + required: + - observedGeneration + type: object + properties: + observedGeneration: + type: integer + format: int64 diff --git a/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_with_included_test/service_file.bal b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_with_included_test/service_file.bal new file mode 100644 index 000000000..22729fb7c --- /dev/null +++ b/openapi-integration-tests/src/test/resources/ballerina_sources/project_openapi_with_included_test/service_file.bal @@ -0,0 +1,61 @@ +import ballerina/http; + +public const RESOURCE_KIND_SERVICE = "Service"; +public const RESOURCE_KIND_POD = "Pod"; + +public type ResourceKind RESOURCE_KIND_SERVICE|RESOURCE_KIND_POD; + +public type Metadata record { + string name; + string displayName?; + string description?; +}; + +public type Status record { + int observedGeneration; +}; + +public type ResourceBase record { + string group; + string version; + ResourceKind kind; + Metadata metadata; +}; + +public type Resource record { + *ResourceBase; + record {} spec; + Status status?; +}; + +public type ServiceSpec record { + string clusterIP; +}; + +public type Service record { + *Resource; + ResourceKind kind = RESOURCE_KIND_SERVICE; + ServiceSpec spec; + Status status?; +}; + +public type PodSpec record { + string nodeName; +}; + +public type Pod record { + *Resource; + ResourceKind kind = RESOURCE_KIND_POD; + PodSpec spec; + Status status?; +}; + +service /payloadV on new http:Listener(7080) { + resource function get Pods() returns Pod[] { + return []; + } + + resource function get Services() returns Service[] { + return []; + } +} diff --git a/openapi-integration-tests/src/test/resources/ballerina_sources/resources/expected_types.bal b/openapi-integration-tests/src/test/resources/ballerina_sources/resources/expected_types.bal new file mode 100644 index 000000000..d7dd22840 --- /dev/null +++ b/openapi-integration-tests/src/test/resources/ballerina_sources/resources/expected_types.bal @@ -0,0 +1,105 @@ +// AUTO-GENERATED FILE. +// This file is auto-generated by the Ballerina OpenAPI tool. + +import ballerina/http; + +public type Status record { + int observedGeneration; +}; + +public type Pod record { + *Resource; + ResourceKind kind = "Pod"; + PodSpec spec; +}; + +public type PodSpec record { + string nodeName; +}; + +# Provides settings related to HTTP/1.x protocol. +public type ClientHttp1Settings record {| + # Specifies whether to reuse a connection for multiple requests + http:KeepAlive keepAlive = http:KEEPALIVE_AUTO; + # The chunking behaviour of the request + http:Chunking chunking = http:CHUNKING_AUTO; + # Proxy server related options + ProxyConfig proxy?; +|}; + +public type ResourceBase record { + string group; + string version; + ResourceKind kind; + Metadata metadata; +}; + +public type Metadata record { + string name; + string displayName?; + string description?; +}; + +public type Resource record { + *ResourceBase; + record {} spec; + Status status?; +}; + +public type ServiceSpec record { + string clusterIP; +}; + +public type Service record { + *Resource; + ResourceKind kind = "Service"; + ServiceSpec spec; +}; + +# Proxy server configurations to be used with the HTTP client endpoint. +public type ProxyConfig record {| + # Host name of the proxy server + string host = ""; + # Proxy server port + int port = 0; + # Proxy server username + string userName = ""; + # Proxy server password + @display {label: "", kind: "password"} + string password = ""; +|}; + +public type ResourceKind "Service"|"Pod"; + +# Provides a set of configurations for controlling the behaviours when communicating with a remote HTTP endpoint. +@display {label: "Connection Config"} +public type ConnectionConfig record {| + # The HTTP version understood by the client + http:HttpVersion httpVersion = http:HTTP_2_0; + # Configurations related to HTTP/1.x protocol + ClientHttp1Settings http1Settings?; + # Configurations related to HTTP/2 protocol + http:ClientHttp2Settings http2Settings?; + # The maximum time to wait (in seconds) for a response before closing the connection + decimal timeout = 60; + # The choice of setting `forwarded`/`x-forwarded` header + string forwarded = "disable"; + # Configurations associated with request pooling + http:PoolConfiguration poolConfig?; + # HTTP caching related configurations + http:CacheConfig cache?; + # Specifies the way of handling compression (`accept-encoding`) header + http:Compression compression = http:COMPRESSION_AUTO; + # Configurations associated with the behaviour of the Circuit Breaker + http:CircuitBreakerConfig circuitBreaker?; + # Configurations associated with retrying + http:RetryConfig retryConfig?; + # Configurations associated with inbound response size limits + http:ResponseLimitConfigs responseLimits?; + # SSL/TLS-related options + http:ClientSecureSocket secureSocket?; + # Proxy server related options + http:ProxyConfig proxy?; + # Enables the inbound payload validation functionality which provided by the constraint package. Enabled by default + boolean validation = true; +|};