diff --git a/.github/workflows/build-ballerina-to-openapi.yml b/.github/workflows/build-ballerina-to-openapi.yml index c313164ba..09c08414b 100644 --- a/.github/workflows/build-ballerina-to-openapi.yml +++ b/.github/workflows/build-ballerina-to-openapi.yml @@ -8,11 +8,11 @@ jobs: if: github.repository_owner == 'ballerina-platform' steps: - uses: actions/checkout@v2 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v2 with: - distribution: 'adopt' - java-version: 17.0.7 + distribution: 'temurin' + java-version: 21.0.3 - name: Change to Timestamped Version run: | startTime=$(TZ="Asia/Kolkata" date +'%Y%m%d-%H%M00') diff --git a/.github/workflows/build-timestamped-master.yml b/.github/workflows/build-timestamped-master.yml index 32e2d1d75..23d20cd67 100644 --- a/.github/workflows/build-timestamped-master.yml +++ b/.github/workflows/build-timestamped-master.yml @@ -11,11 +11,11 @@ jobs: if: github.repository_owner == 'ballerina-platform' steps: - uses: actions/checkout@v2 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v2 with: - distribution: 'adopt' - java-version: 17.0.7 + distribution: 'temurin' + java-version: 21.0.3 - name: Change to Timestamped Version run: | startTime=$(TZ="Asia/Kolkata" date +'%Y%m%d-%H%M00') diff --git a/.github/workflows/central-publish.yml b/.github/workflows/central-publish.yml index 2ce7e868b..342712eac 100644 --- a/.github/workflows/central-publish.yml +++ b/.github/workflows/central-publish.yml @@ -17,11 +17,11 @@ jobs: if: github.repository_owner == 'ballerina-platform' steps: - uses: actions/checkout@v2 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v2 with: - distribution: 'adopt' - java-version: 17.0.7 + distribution: 'temurin' + java-version: 21.0.3 - name: Grant execute permission for gradlew run: chmod +x gradlew diff --git a/.github/workflows/publish-ballerina-to-openapi.yml b/.github/workflows/publish-ballerina-to-openapi.yml index b7d763436..3a0a3aca0 100644 --- a/.github/workflows/publish-ballerina-to-openapi.yml +++ b/.github/workflows/publish-ballerina-to-openapi.yml @@ -11,11 +11,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v2 with: - distribution: 'adopt' - java-version: 17.0.7 + distribution: 'temurin' + java-version: 21.0.3 - name: Set version env variable run: echo "VERSION=$((grep -w "ballerinaToOpenAPIVersion" | cut -d= -f2) < gradle.properties | rev | cut --complement -d- -f1 | rev)" >> $GITHUB_ENV - name: Pre release dependency version update diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index b34e76051..d0d94032c 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -11,11 +11,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v2 with: - distribution: 'adopt' - java-version: 17.0.7 + distribution: 'temurin' + java-version: 21.0.3 - name: Check for Ballerina to OpenAPI release run: | BAL_TO_OPENAPI_VERSION=$(grep -w "ballerinaToOpenAPIVersion" gradle.properties | cut -d= -f2) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index d912a4fcb..ce6bb41a8 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -10,11 +10,11 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v2 with: - distribution: 'adopt' - java-version: 17.0.7 + distribution: 'temurin' + java-version: 21.0.3 - name: Build with Gradle env: packageUser: ${{ github.actor }} @@ -32,11 +32,11 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v2 with: - distribution: 'adopt' - java-version: 17.0.7 + distribution: 'temurin' + java-version: 21.0.3 - name: Build with Gradle env: packageUser: ${{ github.actor }} diff --git a/README.md b/README.md index 92b773265..06eeefcec 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ For more information on the supported operations, go to [Using the OpenAPI Tools ### Setting Up the Prerequisites -1. OpenJDK 11 ([Adopt OpenJDK](https://adoptopenjdk.net/) or any other OpenJDK distribution) +1. OpenJDK 21 ([Adopt OpenJDK](https://adoptopenjdk.net/) or any other OpenJDK distribution) >**Info:** You can also use [Oracle JDK](https://www.oracle.com/java/technologies/javase-downloads.html). Set the JAVA_HOME environment variable to the pathname of the directory into which you installed JDK. 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 c8821b2b7..289116ceb 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 @@ -80,7 +80,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 8e6aceaad..ea4effecb 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 @@ -71,7 +71,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 8833b1743..187671add 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 @@ -21,6 +21,7 @@ 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; @@ -49,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.Constants.JSON_DATA; import static io.ballerina.openapi.service.mapper.Constants.NAME_CONFIG; import static io.ballerina.openapi.service.mapper.Constants.VALUE; @@ -76,14 +78,17 @@ 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); - - Map properties = mapRecordFields(recordFieldMap, components, requiredFields, - recordName, false, additionalData); + List allOfSchemaList = mapIncludedRecords(typeSymbol, components, recordFieldMap, additionalData, + recordName, fieldsOnlyForRequiredList); + RecordFieldMappingContext mappingContext = new RecordFieldMappingContext( + recordFieldMap, components, requiredFields, recordName, false, + true, additionalData, fieldsOnlyForRequiredList); + Map properties = mapRecordFields(mappingContext); Optional restFieldType = typeSymbol.restTypeDescriptor(); if (restFieldType.isPresent()) { @@ -95,8 +100,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); @@ -108,7 +113,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) { @@ -125,19 +131,148 @@ 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, - boolean inferNameFromJsonData, - 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 inferNameFromJsonData Flag indicating whether field names should be inferred from JSON data. + * @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, + boolean inferNameFromJsonData, + 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(); + boolean inferNameFromJsonData = context.inferNameFromJsonData(); + AdditionalData additionalData = context.additionalData(); + Set fieldsOnlyForRequiredList = context.fieldsOnlyForRequiredList(); Map properties = new LinkedHashMap<>(); + for (Map.Entry recordField : recordFieldMap.entrySet()) { RecordFieldSymbol recordFieldSymbol = recordField.getValue(); String recordFieldName = getRecordFieldName(inferNameFromJsonData, recordField, @@ -146,6 +281,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); @@ -154,7 +292,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 { @@ -168,14 +306,6 @@ public static Map mapRecordFields(Map return properties; } - public static Map mapRecordFields(Map recordFieldMap, - Components components, Set requiredFields, - String recordName, boolean treatNilableAsOptional, - AdditionalData additionalData) { - return mapRecordFields(recordFieldMap, components, requiredFields, recordName, treatNilableAsOptional, - true, additionalData); - } - private static String getRecordFieldName(boolean inferNameFromJsonData, Map.Entry recordFieldEntry, SemanticModel semanticModel) { @@ -185,17 +315,19 @@ private static String getRecordFieldName(boolean inferNameFromJsonData, } 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) @@ -206,6 +338,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 b59b29737..160e6243c 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,11 @@ 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, false, componentMapperData); + + RecordTypeMapper.RecordFieldMappingContext context = new RecordTypeMapper.RecordFieldMappingContext( + recordFieldMap, components, requiredFields, recordName, treatNilableAsOptional, + false, 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 1b2b39f12..53c03278d 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 @@ -570,6 +570,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/build.gradle b/build.gradle index d1645abd0..3137bf9a7 100644 --- a/build.gradle +++ b/build.gradle @@ -17,10 +17,10 @@ plugins { id "base" id 'maven-publish' - id "com.github.spotbugs" version "5.0.14" - id "com.github.johnrengelman.shadow" version "8.1.1" - id "de.undercouch.download" version "5.4.0" - id "net.researchgate.release" version "2.8.0" + id "com.github.spotbugs" version "${spotbugsPluginVersion}" + id "com.github.johnrengelman.shadow" version "${shadowJarPluginVersion}" + id "de.undercouch.download" version "${downloadPluginVersion}" + id "net.researchgate.release" version "${releasePluginVersion}" id 'org.javamodularity.moduleplugin' version '1.7.0' apply false id "org.sonarqube" version "4.0.0.2929" } @@ -54,7 +54,7 @@ ext.stdlibTaskVersion = project.stdlibTaskVersion ext.stdlibFileVersion = project.stdlibFileVersion ext.stdlibHttpVersion = project.stdlibHttpVersion ext.stdlibUrlVersion = project.stdlibUrlVersion -ext.stdlibXmldataVersion = project.stdlibXmldataVersion +ext.stdlibDataXmldataVersion = project.stdlibDataXmldataVersion ext.stdlibGraphqlVersion = project.stdlibGraphqlVersion ext.stdlibGrpcVersion = project.stdlibGrpcVersion ext.stdlibWebsubVersion = project.stdlibWebsubVersion @@ -129,7 +129,7 @@ subprojects { ballerinaStdLibs "io.ballerina.stdlib:oauth2-ballerina:${stdlibOAuth2Version}" ballerinaStdLibs "io.ballerina.stdlib:uuid-ballerina:${stdlibUuidVersion}" ballerinaStdLibs "io.ballerina.stdlib:url-ballerina:${stdlibUrlVersion}" - ballerinaStdLibs "io.ballerina.lib:data.xmldata-ballerina:${stdlibXmldataVersion}" + ballerinaStdLibs "io.ballerina.lib:data.xmldata-ballerina:${stdlibDataXmldataVersion}" ballerinaStdLibs "io.ballerina.stdlib:observe-ballerina:${observeVersion}" ballerinaStdLibs "io.ballerina:observe-ballerina:${observeInternalVersion}" ballerinaStdLibs "io.ballerina.stdlib:graphql-ballerina:${stdlibGraphqlVersion}" diff --git a/gradle.properties b/gradle.properties index 70492de42..087560724 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,13 +4,18 @@ version=2.1.1-SNAPSHOT ballerinaToOpenAPIVersion=2.1.1-SNAPSHOT +spotbugsPluginVersion=6.0.18 +shadowJarPluginVersion=8.1.1 +downloadPluginVersion=5.4.0 +releasePluginVersion=2.8.0 + # Client Native Version clientNativeVersion=1.0.1-SNAPSHOT # Mark this as false to skip publishing the client native artifacts clientNativePublish=false #dependency -ballerinaLangVersion=2201.11.0-20241008-112400-81975006 +ballerinaLangVersion=2201.11.0-20241117-133400-a3054b77 testngVersion=7.6.1 slf4jVersion=1.7.30 org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 @@ -24,45 +29,45 @@ puppycrawlCheckstyleVersion = 10.12.1 commonsCodecVersion=1.16.0 # Stdlib Level 01 -stdlibIoVersion=1.6.1 -stdlibRegexVersion=1.4.3 -stdlibTimeVersion=2.4.0 -stdlibUrlVersion=2.4.0 -stdlibXmldataVersion=1.0.0 +stdlibIoVersion=1.6.2-20241112-233100-995cf5f +stdlibRegexVersion=1.4.4-20241108-130200-fcd3fce +stdlibTimeVersion=2.6.0-20241113-073800-201b904 +stdlibUrlVersion=2.4.1-20241113-073900-335ff51 +stdlibDataXmldataVersion=1.0.1-20241113-122800-f4e815c # Stdlib Level 02 -stdlibConstraintVersion=1.5.0 -stdlibCryptoVersion=2.7.2 -stdlibLogVersion=2.10.0 -stdlibOsVersion=1.8.0 -stdlibTaskVersion=2.5.0 +stdlibConstraintVersion=1.6.0-20241113-090900-d276ad5 +stdlibCryptoVersion=2.7.3-20241113-081400-d015a39 +stdlibLogVersion=2.10.1-20241113-120000-4577868 +stdlibOsVersion=1.8.1-20241113-122000-cca973b +stdlibTaskVersion=2.5.1-20241113-123500-f905281 # Stdlib Level 03 -stdlibCacheVersion=3.8.0 -stdlibFileVersion=1.10.0 -stdlibMimeVersion=2.10.0 -stdlibUuidVersion=1.8.0 +stdlibCacheVersion=3.8.1-20241113-125700-b75a1bf +stdlibFileVersion=1.10.1-20241113-151700-e1a2e38 +stdlibMimeVersion=2.10.2-20241113-154200-d953747 +stdlibUuidVersion=1.8.1-20241113-154400-443c67b # Stdlib Level 04 -stdlibAuthVersion=2.12.0 -stdlibDataJsonDataVersion=0.3.0-20241105-101100-661d11f -stdlibJwtVersion=2.13.0 -stdlibOAuth2Version=2.12.0 +stdlibAuthVersion=2.12.1-20241113-162300-ded40eb +stdlibDataJsonDataVersion=0.3.0-20241114-143900-285d739 +stdlibJwtVersion=2.13.1-20241113-162400-b59ccfa +stdlibOAuth2Version=2.12.1-20241113-162400-4c6ddfe # Stdlib Level 05 -stdlibHttpVersion=2.13.0-20241106-120000-d375c3b +stdlibHttpVersion=2.13.0-20241114-182900-7e9f66a # Stdlib Level 06 -stdlibGrpcVersion=1.12.0 -stdlibWebsocketVersion=2.12.0 -stdlibWebsubVersion=2.12.0 +stdlibGrpcVersion=1.13.0-20241114-195700-5188f60 +stdlibWebsocketVersion=2.13.0-20241116-202000-ddd958d +stdlibWebsubVersion=2.13.0-20241114-233100-73205d6 # Stdlib Level 07 -stdlibGraphqlVersion=1.14.0 +stdlibGraphqlVersion=1.15.0-20241117-164000-4d95b39 # Ballerinax Observer -observeVersion=1.3.0 -observeInternalVersion=1.3.0 +observeVersion=1.4.0-20241113-092000-b83ae74 +observeInternalVersion=1.3.1-20241113-101700-265054d # Enabled publishing insecure checksums, due to fail to publish to maven central # Refer https://github.com/gradle/gradle/issues/11308 diff --git a/gradle/javaProject.gradle b/gradle/javaProject.gradle index 7d89a86b1..5fd57b0cc 100644 --- a/gradle/javaProject.gradle +++ b/gradle/javaProject.gradle @@ -69,7 +69,7 @@ dependencies { checkstyle "com.puppycrawl.tools:checkstyle:${project.puppycrawlCheckstyleVersion}" } -sourceCompatibility = JavaVersion.VERSION_17 +sourceCompatibility = JavaVersion.VERSION_21 def excludePattern = '**/module-info.java' tasks.withType(Checkstyle) { @@ -140,9 +140,12 @@ checkstyle { } spotbugsMain { + def classLoader = plugins["com.github.spotbugs"].class.classLoader + def SpotBugsConfidence = classLoader.findLoadedClass("com.github.spotbugs.snom.Confidence") + def SpotBugsEffort = classLoader.findLoadedClass("com.github.spotbugs.snom.Effort") + effort = SpotBugsEffort.MAX + reportLevel = SpotBugsConfidence.LOW ignoreFailures = true - effort = "max" - reportLevel = "low" reportsDir = file("$project.buildDir/reports/spotbugs") def excludeFile = file("${rootDir}/spotbugs-exclude.xml") if (excludeFile.exists()) { diff --git a/module-ballerina-openapi/Ballerina.toml b/module-ballerina-openapi/Ballerina.toml index 54423c2fc..1c3e53d73 100644 --- a/module-ballerina-openapi/Ballerina.toml +++ b/module-ballerina-openapi/Ballerina.toml @@ -1,4 +1,4 @@ [package] org= "ballerina" name= "openapi" -version= "@toml.version@" +version= "2.1.1" diff --git a/module-ballerina-openapi/CompilerPlugin.toml b/module-ballerina-openapi/CompilerPlugin.toml index c4c4205c0..57d0fee57 100644 --- a/module-ballerina-openapi/CompilerPlugin.toml +++ b/module-ballerina-openapi/CompilerPlugin.toml @@ -3,7 +3,7 @@ id = "openapi-tools" class = "io.ballerina.openapi.validator.OpenAPIValidatorPlugin" [[dependency]] -path = "../openapi-validator/build/libs/openapi-validator-@project.version@.jar" +path = "../openapi-validator/build/libs/openapi-validator-2.1.1-SNAPSHOT.jar" groupId = "ballerina" artifactId = "openapi" -version = "@project.version@." +version = "2.1.1-SNAPSHOT" diff --git a/module-ballerina-openapi/build.gradle b/module-ballerina-openapi/build.gradle index c7d5fc188..631452dd2 100644 --- a/module-ballerina-openapi/build.gradle +++ b/module-ballerina-openapi/build.gradle @@ -47,7 +47,7 @@ jar { def packageName = "openapi" def packageOrg = "ballerina" -def platform = "java17" +def platform = "java21" def tomlVersion = stripBallerinaExtensionVersion("${project.version}") def ballerinaConfigFilePlaceHolder = new File("${project.rootDir}/config/resources/Ballerina.toml") def ballerinaCompileConfigFilePlaceHolder = new File("${project.rootDir}/config/resources/CompilerPlugin.toml") diff --git a/openapi-bal-task-plugin/src/main/java/io/ballerina/openapi/bal/tool/OpenAPICodeGeneratorTool.java b/openapi-bal-task-plugin/src/main/java/io/ballerina/openapi/bal/tool/OpenAPICodeGeneratorTool.java index 48513d3a2..e457ddd6f 100644 --- a/openapi-bal-task-plugin/src/main/java/io/ballerina/openapi/bal/tool/OpenAPICodeGeneratorTool.java +++ b/openapi-bal-task-plugin/src/main/java/io/ballerina/openapi/bal/tool/OpenAPICodeGeneratorTool.java @@ -413,8 +413,8 @@ private boolean clientNativeDependencyAlreadyExist(String version, ToolContext t project = BuildProject.load(path); Map platforms = project.currentPackage().manifest().platforms(); - if (Objects.nonNull(platforms) && platforms.containsKey("java17")) { - Optional> nativeDependency = platforms.get("java17").dependencies().stream().filter( + if (Objects.nonNull(platforms) && platforms.containsKey("java21")) { + Optional> nativeDependency = platforms.get("java21").dependencies().stream().filter( dependency -> dependency.containsKey("groupId") && dependency.get("groupId").equals("io.ballerina.openapi") && dependency.containsKey("artifactId") && @@ -439,7 +439,7 @@ private boolean clientNativeDependencyAlreadyExist(String version, ToolContext t private NodeList populateClientNativeDependency( NodeList tomlMembers, String version) { String desc = "This dependency is added automatically by the OpenAPI tool. DO NOT REMOVE UNLESS REQUIRED"; - tomlMembers = tomlMembers.add(SampleNodeGenerator.createTableArray("platform.java17.dependency", desc)); + tomlMembers = tomlMembers.add(SampleNodeGenerator.createTableArray("platform.java21.dependency", desc)); tomlMembers = tomlMembers.add(SampleNodeGenerator.createStringKV("groupId", "io.ballerina.openapi", null)); tomlMembers = tomlMembers.add(SampleNodeGenerator.createStringKV("artifactId", "client-native", null)); tomlMembers = tomlMembers.add(SampleNodeGenerator.createStringKV("version", version, null)); diff --git a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/OpenApiCmd.java b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/OpenApiCmd.java index 28af20e0c..62ae58d28 100644 --- a/openapi-cli/src/main/java/io/ballerina/openapi/cmd/OpenApiCmd.java +++ b/openapi-cli/src/main/java/io/ballerina/openapi/cmd/OpenApiCmd.java @@ -536,8 +536,8 @@ private void updateBallerinaTomlWithClientNativeDependency() { private boolean clientNativeDependencyAlreadyExist(String version) { Project project = ProjectLoader.loadProject(executionPath); Map platforms = project.currentPackage().manifest().platforms(); - if (Objects.nonNull(platforms) && platforms.containsKey("java17")) { - Optional> nativeDependency = platforms.get("java17").dependencies().stream().filter( + if (Objects.nonNull(platforms) && platforms.containsKey("java21")) { + Optional> nativeDependency = platforms.get("java21").dependencies().stream().filter( dependency -> dependency.containsKey("groupId") && dependency.get("groupId").equals("io.ballerina.openapi") && dependency.containsKey("artifactId") && @@ -565,7 +565,7 @@ private boolean clientNativeDependencyAlreadyExist(String version) { private NodeList populateClientNativeDependency( NodeList tomlMembers, String version) { String desc = "This dependency is added automatically by the OpenAPI tool. DO NOT REMOVE UNLESS REQUIRED"; - tomlMembers = tomlMembers.add(SampleNodeGenerator.createTableArray("platform.java17.dependency", desc)); + tomlMembers = tomlMembers.add(SampleNodeGenerator.createTableArray("platform.java21.dependency", desc)); tomlMembers = tomlMembers.add(SampleNodeGenerator.createStringKV("groupId", "io.ballerina.openapi", null)); tomlMembers = tomlMembers.add(SampleNodeGenerator.createStringKV("artifactId", "client-native", null)); tomlMembers = tomlMembers.add(SampleNodeGenerator.createStringKV("version", version, null)); 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 a3421918d..a0ca17522 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 @@ -137,6 +137,12 @@ public void testRecordWithNameFieldAnnotation() throws IOException { TestUtils.compareWithGeneratedFile(ballerinaFilePath, "record/record_field_with_name_annotation.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 a8b960672..a7887f655 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: header32 + in: header + required: true + schema: + type: array + items: + type: integer + format: int64 + default: + - 1 + - 2 + - 3 + - name: header31 + in: header + schema: + type: string + default: Pod 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/post_method.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/post_method.yaml index 0312dbb74..bed834dcf 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/post_method.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/post_method.yaml @@ -37,9 +37,6 @@ paths: "201": description: Created headers: - Last-Modified: - schema: - type: string Cache-Control: schema: type: string @@ -47,6 +44,9 @@ paths: ETag: schema: type: string + Last-Modified: + schema: + type: string content: text/plain: schema: 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/expected_gen/response/cache_config_02.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_02.yaml index 57f8a75c9..ec35aaa18 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_02.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_02.yaml @@ -57,9 +57,6 @@ paths: "200": description: Ok headers: - Last-Modified: - schema: - type: string Cache-Control: schema: type: string @@ -67,6 +64,9 @@ paths: ETag: schema: type: string + Last-Modified: + schema: + type: string content: text/plain: schema: diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_03.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_03.yaml index c2d7fb5d2..a07a7d1a5 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_03.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_03.yaml @@ -17,9 +17,6 @@ paths: "200": description: Ok headers: - Last-Modified: - schema: - type: string Cache-Control: schema: type: string @@ -28,6 +25,9 @@ paths: ETag: schema: type: string + Last-Modified: + schema: + type: string content: text/plain: schema: diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_04.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_04.yaml index 29951190d..5853c6d95 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_04.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_04.yaml @@ -6,9 +6,6 @@ paths: "200": description: Ok headers: - Last-Modified: - schema: - type: string Cache-Control: schema: type: string @@ -16,6 +13,9 @@ paths: ETag: schema: type: string + Last-Modified: + schema: + type: string content: text/plain: schema: diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_05.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_05.yaml index 4b0c7d055..4d36136b5 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_05.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_05.yaml @@ -6,9 +6,6 @@ paths: "200": description: Ok headers: - Last-Modified: - schema: - type: string Cache-Control: schema: type: string @@ -16,6 +13,9 @@ paths: ETag: schema: type: string + Last-Modified: + schema: + type: string content: text/plain: schema: @@ -27,9 +27,6 @@ paths: "200": description: Ok headers: - Last-Modified: - schema: - type: string Cache-Control: schema: type: string @@ -37,6 +34,9 @@ paths: ETag: schema: type: string + Last-Modified: + schema: + type: string content: text/plain: schema: diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_06.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_06.yaml index b9188c400..8ae4c8c73 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_06.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/cache_config_06.yaml @@ -17,9 +17,6 @@ paths: "200": description: Ok headers: - Last-Modified: - schema: - type: string Cache-Control: schema: type: string @@ -27,6 +24,9 @@ paths: ETag: schema: type: string + Last-Modified: + schema: + type: string content: text/plain: schema: @@ -44,9 +44,6 @@ paths: "200": description: Ok headers: - Last-Modified: - schema: - type: string Cache-Control: schema: type: string @@ -54,6 +51,9 @@ paths: ETag: schema: type: string + Last-Modified: + schema: + type: string content: text/plain: schema: @@ -61,9 +61,6 @@ paths: "202": description: Accepted headers: - Last-Modified: - schema: - type: string Cache-Control: schema: type: string @@ -71,6 +68,9 @@ paths: ETag: schema: type: string + Last-Modified: + schema: + type: string "500": description: InternalServerError content: diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/configuration_rs01.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/configuration_rs01.yaml index a573a557d..45e53c3fc 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/configuration_rs01.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/response/configuration_rs01.yaml @@ -17,9 +17,6 @@ paths: "200": description: Ok headers: - Last-Modified: - schema: - type: string Cache-Control: schema: type: string @@ -27,6 +24,9 @@ paths: ETag: schema: type: string + Last-Modified: + schema: + type: string content: text/plain: schema: @@ -38,9 +38,6 @@ paths: "200": description: Ok headers: - Last-Modified: - schema: - type: string Cache-Control: schema: type: string @@ -48,6 +45,9 @@ paths: ETag: schema: type: string + Last-Modified: + schema: + type: string content: text/plain: schema: diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/snowpeak.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/snowpeak.yaml index e95541fab..6412d43b0 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/snowpeak.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/snowpeak.yaml @@ -18,9 +18,6 @@ paths: "200": description: Ok headers: - Last-Modified: - schema: - type: string Cache-Control: schema: type: string @@ -28,6 +25,9 @@ paths: ETag: schema: type: string + Last-Modified: + schema: + type: string content: application/json: schema: @@ -112,9 +112,6 @@ paths: "201": description: Created headers: - Last-Modified: - schema: - type: string Cache-Control: schema: type: string @@ -122,6 +119,9 @@ paths: ETag: schema: type: string + Last-Modified: + schema: + type: string content: application/json: schema: diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/snowpeak_cache.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/snowpeak_cache.yaml index 5315a6f8a..f5e218d14 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/snowpeak_cache.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/snowpeak_cache.yaml @@ -18,9 +18,6 @@ paths: "200": description: Ok headers: - Last-Modified: - schema: - type: string Cache-Control: schema: type: string @@ -28,6 +25,9 @@ paths: ETag: schema: type: string + Last-Modified: + schema: + type: string content: application/json: schema: diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/snowpeak_request_body_ref.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/snowpeak_request_body_ref.yaml index e72cacc4c..2fba99fe1 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/snowpeak_request_body_ref.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/snowpeak_request_body_ref.yaml @@ -25,9 +25,6 @@ paths: "201": description: Created headers: - Last-Modified: - schema: - type: string Cache-Control: schema: type: string @@ -35,6 +32,9 @@ paths: ETag: schema: type: string + Last-Modified: + schema: + type: string content: application/json: schema: @@ -68,9 +68,6 @@ paths: "201": description: Created headers: - Last-Modified: - schema: - type: string Cache-Control: schema: type: string @@ -78,6 +75,9 @@ paths: ETag: schema: type: string + Last-Modified: + schema: + type: string "409": description: Conflict content: 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/tests/test.bal b/openapi-client-native/ballerina-tests/tests/test.bal index 7ed27a4ed..5af604960 100644 --- a/openapi-client-native/ballerina-tests/tests/test.bal +++ b/openapi-client-native/ballerina-tests/tests/test.bal @@ -512,7 +512,7 @@ function testInvalidImplFunctionSignature() { OkAlbumArray|NotFoundErrorMessage|error res = albumClient->getAlbums3(genre = "Hard Rock"); if res is error { test:assertTrue(res is ClientMethodInvocationError); - test:assertTrue(res.message().includes("client method invocation failed: java.lang.ClassCastException: " + + test:assertTrue(res.message().includes("error in invoking client remote method: " + "class io.ballerina.runtime.internal.values.TypedescValueImpl cannot be cast to class io.ballerina.runtime.internal.values.MapValueImpl")); } else { test:assertFail("invalid response type"); diff --git a/openapi-client-native/build-configs/resources/Ballerina.toml b/openapi-client-native/build-configs/resources/Ballerina.toml index 76ed1fb53..0c54a276e 100644 --- a/openapi-client-native/build-configs/resources/Ballerina.toml +++ b/openapi-client-native/build-configs/resources/Ballerina.toml @@ -2,9 +2,9 @@ org = "ballerina" name = "client_native_test" version = "0.1.0" -distribution = "2201.8.6" +distribution = "2201.11.0-20241117-133400-a3054b77" -[[platform.java17.dependency]] +[[platform.java21.dependency]] groupId = "io.ballerina.openapi" artifactId = "client-native" version = "@client.native.version@" diff --git a/openapi-client-native/build.gradle b/openapi-client-native/build.gradle index b9f83783f..cff460571 100644 --- a/openapi-client-native/build.gradle +++ b/openapi-client-native/build.gradle @@ -51,9 +51,12 @@ checkstyleMain.dependsOn(":checkstyle:downloadCheckstyleRuleFiles") checkstyleTest.dependsOn(":checkstyle:downloadCheckstyleRuleFiles") spotbugsMain { + def classLoader = plugins["com.github.spotbugs"].class.classLoader + def SpotBugsConfidence = classLoader.findLoadedClass("com.github.spotbugs.snom.Confidence") + def SpotBugsEffort = classLoader.findLoadedClass("com.github.spotbugs.snom.Effort") + effort = SpotBugsEffort.MAX + reportLevel = SpotBugsConfidence.LOW ignoreFailures = true - effort = "max" - reportLevel = "low" reportsDir = file("$project.buildDir/reports/spotbugs") def excludeFile = file("${rootDir}/spotbugs-exclude.xml") if (excludeFile.exists()) { diff --git a/openapi-client-native/src/main/java/io/ballerina/openapi/client/GeneratedClient.java b/openapi-client-native/src/main/java/io/ballerina/openapi/client/GeneratedClient.java index 16ad1c76f..c3564edb5 100644 --- a/openapi-client-native/src/main/java/io/ballerina/openapi/client/GeneratedClient.java +++ b/openapi-client-native/src/main/java/io/ballerina/openapi/client/GeneratedClient.java @@ -18,20 +18,10 @@ package io.ballerina.openapi.client; import io.ballerina.runtime.api.Environment; -import io.ballerina.runtime.api.Future; -import io.ballerina.runtime.api.PredefinedTypes; -import io.ballerina.runtime.api.async.Callback; import io.ballerina.runtime.api.values.BArray; import io.ballerina.runtime.api.values.BError; import io.ballerina.runtime.api.values.BObject; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -import static io.ballerina.runtime.api.constants.RuntimeConstants.CURRENT_TRANSACTION_CONTEXT_PROPERTY; -import static io.ballerina.runtime.observability.ObservabilityConstants.KEY_OBSERVER_CONTEXT; - /** * This class contains the generated client's native code. * @@ -39,12 +29,6 @@ */ public class GeneratedClient { - static final String MAIN_STRAND = "MAIN_STRAND"; - static final String SRC_HANDLER = "SRC_HANDLER"; - static final String REMOTE_ADDRESS = "REMOTE_ADDRESS"; - static final String ORIGIN_HOST = "ORIGIN_HOST"; - static final String POOLED_BYTE_BUFFER_FACTORY = "POOLED_BYTE_BUFFER_FACTORY"; - public static Object invokeResource(Environment env, BObject client, BArray pathParams, BArray params) { String functionName = env.getFunctionName(); try { @@ -80,14 +64,12 @@ private static Object invokeClientMethod(Environment env, BObject client, BArray int pathLength = (int) path.getLength(); int paramLength = (int) params.getLength(); - Object[] paramFeed = new Object[(pathLength + paramLength) * 2]; + Object[] paramFeed = new Object[pathLength + paramLength]; for (int i = 0; i < pathLength; i++) { - paramFeed[i * 2] = path.get(i); - paramFeed[i * 2 + 1] = true; + paramFeed[i] = path.get(i); } for (int i = 0; i < paramLength; i++) { - paramFeed[(pathLength + i) * 2] = params.get(i); - paramFeed[(pathLength + i) * 2 + 1] = true; + paramFeed[pathLength + i] = params.get(i); } return invokeClientMethod(env, client, methodName, paramFeed); @@ -97,49 +79,22 @@ private static Object invokeClientMethod(Environment env, BObject client, BArray String methodName) { int paramLength = (int) params.getLength(); - Object[] paramFeed = new Object[paramLength * 2]; + Object[] paramFeed = new Object[paramLength]; for (int i = 0; i < paramLength; i++) { - paramFeed[i * 2] = params.get(i); - paramFeed[i * 2 + 1] = true; + paramFeed[i] = params.get(i); } return invokeClientMethod(env, client, methodName, paramFeed); } private static Object invokeClientMethod(Environment env, BObject client, String methodName, Object[] paramFeed) { - Future balFuture = env.markAsync(); - Map propertyMap = getPropertiesToPropagate(env); - env.getRuntime().invokeMethodAsyncSequentially(client, methodName, null, null, new Callback() { - @Override - public void notifySuccess(Object result) { - balFuture.complete(result); - } - - @Override - public void notifyFailure(BError bError) { - BError invocationError = ClientUtil.createHttpError("client method invocation failed: " + - bError.getErrorMessage(), bError); - balFuture.complete(invocationError); + return env.yieldAndRun(() -> { + try { + return env.getRuntime().callMethod(client, methodName, null, paramFeed); + } catch (BError bError) { + return ClientUtil.createHttpError("client method invocation failed: " + + bError.getErrorMessage(), bError); } - }, propertyMap, PredefinedTypes.TYPE_NULL, paramFeed); - return null; - } - - private static Map getPropertiesToPropagate(Environment env) { - String[] keys = {CURRENT_TRANSACTION_CONTEXT_PROPERTY, KEY_OBSERVER_CONTEXT, SRC_HANDLER, MAIN_STRAND, - POOLED_BYTE_BUFFER_FACTORY, REMOTE_ADDRESS, ORIGIN_HOST}; - Map subMap = new HashMap<>(); - for (String key : keys) { - Object value = env.getStrandLocal(key); - if (value != null) { - subMap.put(key, value); - } - } - String strandParentFunctionName = Objects.isNull(env.getStrandMetadata()) ? null : - env.getStrandMetadata().getParentFunctionName(); - if (Objects.nonNull(strandParentFunctionName) && strandParentFunctionName.equals("onMessage")) { - subMap.put(MAIN_STRAND, true); - } - return subMap; + }); } } diff --git a/openapi-core/src/main/java/io/ballerina/openapi/core/generators/common/TypeHandler.java b/openapi-core/src/main/java/io/ballerina/openapi/core/generators/common/TypeHandler.java index 628d2255e..305194bcb 100644 --- a/openapi-core/src/main/java/io/ballerina/openapi/core/generators/common/TypeHandler.java +++ b/openapi-core/src/main/java/io/ballerina/openapi/core/generators/common/TypeHandler.java @@ -81,7 +81,7 @@ public class TypeHandler { private static TypeHandler typeHandlerInstance; private static BallerinaTypesGenerator ballerinaTypesGenerator; - public HashMap typeDefinitionNodes = new HashMap<>(); + private HashMap typeDefinitionNodes = new HashMap<>(); private final Set imports = new LinkedHashSet<>(); private static List constraintDiagnostics; 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 74c2d4fbb..c915afa38 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); + } + @Test(description = "Generate with openapi serviceConfig annotation to service contract type") public void openapiServiceConfigForServiceContractType() throws IOException, InterruptedException { executeCommand("project_openapi_info_with_service_contract_type/service_file.bal", 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 996094380..f7a6c36c2 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.5.0 + version: 2.6.0 modulePrefix: time name: Date DateFields: @@ -81,7 +81,7 @@ components: orgName: ballerina pkgName: time moduleName: time - version: 2.5.0 + version: 2.6.0 modulePrefix: time name: DateFields OptionalTimeOfDayFields: @@ -100,7 +100,7 @@ components: orgName: ballerina pkgName: time moduleName: time - version: 2.5.0 + version: 2.6.0 modulePrefix: time name: OptionalTimeOfDayFields Seconds: @@ -111,7 +111,7 @@ components: orgName: ballerina pkgName: time moduleName: time - version: 2.5.0 + version: 2.6.0 modulePrefix: time name: Seconds Student: @@ -165,6 +165,6 @@ components: orgName: ballerina pkgName: time moduleName: time - version: 2.5.0 + version: 2.6.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; +|}; diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index 9c24bf709..363627fb4 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -34,4 +34,12 @@ + + + + + + + +