From 6eadee34e97ee567be37a7bd5b5a3350c0513877 Mon Sep 17 00:00:00 2001 From: lnash94 Date: Mon, 25 Jul 2022 09:06:39 +0530 Subject: [PATCH 1/5] Add constraint support ballerina to openapi record field --- .../service/ConstraintAnnotation.java | 124 ++++++++++++++++++ .../service/OpenAPIComponentMapper.java | 19 +++ .../generators/openapi/ConstraintTests.java | 40 ++++++ .../constraint/record_field.bal | 25 ++++ openapi-cli/src/test/resources/testng.xml | 1 + 5 files changed, 209 insertions(+) create mode 100644 openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/ConstraintAnnotation.java create mode 100644 openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ConstraintTests.java create mode 100644 openapi-cli/src/test/resources/ballerina-to-openapi/constraint/record_field.bal diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/ConstraintAnnotation.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/ConstraintAnnotation.java new file mode 100644 index 000000000..b5076bf51 --- /dev/null +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/ConstraintAnnotation.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.ballerina.openapi.converter.service; + + +/** + * + * @since 1.2.0 + */ +public class ConstraintAnnotation { + private final String minValue; + private final String maxValue; + private final String length; + private final String minLength; + private final String maxLength; + private final String minValueExclusive; + private final String maxValueExclusive; + + public ConstraintAnnotation(ConstraintAnnotationBuilder builder) { + this.minValue = builder.minValue; + this.maxValue = builder.maxValue; + this.length = builder.length; + this.minLength = builder.minLength; + this.maxLength = builder.maxLength; + this.minValueExclusive = builder.minValueExclusive; + this.maxValueExclusive = builder.maxValueExclusive; + } + + public String getMinValue() { + return minValue; + } + + public String getMaxValue() { + return maxValue; + } + + public String getLength() { + return length; + } + + public String getMinLength() { + return minLength; + } + + public String getMaxLength() { + return maxLength; + } + + public String getMinValueExclusive() { + return minValueExclusive; + } + + public String getMaxValueExclusive() { + return maxValueExclusive; + } + + /** + * This is the builder class for the {@link ConstraintAnnotation}. + */ + public static class ConstraintAnnotationBuilder { + private String minValue; + private String maxValue; + private String length; + private String minLength; + private String maxLength; + private String minValueExclusive; + private String maxValueExclusive; + + public ConstraintAnnotationBuilder withMinValue(String minValue) { + this.minValue = minValue; + return this; + } + + public ConstraintAnnotationBuilder withLength(String length) { + this.length = length; + return this; + } + + public ConstraintAnnotationBuilder setMaxValue(String maxValue) { + this.maxValue = maxValue; + return this; + } + + public ConstraintAnnotationBuilder withMinLength(String minLength) { + this.minLength = minLength; + return this; + } + + public ConstraintAnnotationBuilder withMaxLength(String maxLength) { + this.maxLength = maxLength; + return this; + } + + public ConstraintAnnotationBuilder withMinValueExclusive(String minValueExclusive) { + this.minValueExclusive = minValueExclusive; + return this; + } + + public ConstraintAnnotationBuilder withMaxValueExclusive(String maxValueExclusive) { + this.maxValueExclusive = maxValueExclusive; + return this; + } + + public ConstraintAnnotation build() { + ConstraintAnnotation constraintAnnotation = new ConstraintAnnotation(this); + return constraintAnnotation; + } + } +} diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java index daf5da7ff..51fb63a61 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java @@ -18,6 +18,7 @@ package io.ballerina.openapi.converter.service; +import io.ballerina.compiler.api.symbols.AnnotationSymbol; import io.ballerina.compiler.api.symbols.ArrayTypeSymbol; import io.ballerina.compiler.api.symbols.ConstantSymbol; import io.ballerina.compiler.api.symbols.Documentable; @@ -266,6 +267,24 @@ private ObjectSchema generateObjectSchemaFromRecordFields(Map sc componentSchema.setDescription(apiDocs.get(componentName)); Map schemaProperties = new LinkedHashMap<>(); for (Map.Entry field: rfields.entrySet()) { + List annotations = field.getValue().annotations(); + ((RecordTypeSymbol)((TypeReferenceTypeSymbol)field.getValue().annotations().get(0).typeDescriptor().get()).typeDescriptor()).fieldDescriptors(); + annotations.stream().filter(annotSymbol->annotSymbol.getModule().get().getName().get().equals("constraint")) + .forEach(value-> { + ConstraintAnnotation.ConstraintAnnotationBuilder constraintAnnotBuilder = + new ConstraintAnnotation.ConstraintAnnotationBuilder(); + Map recordFields = + ((RecordTypeSymbol) ((TypeReferenceTypeSymbol) value.typeDescriptor() + .get()).typeDescriptor()).fieldDescriptors(); + recordFields.forEach((k,v) -> { + RecordFieldSymbol fieldSymbol = v; + switch (k) { + case "length": +// fieldSymbol. + } + }); + }); + String fieldName = ConverterCommonUtils.unescapeIdentifier(field.getKey().trim()); if (!field.getValue().isOptional()) { required.add(fieldName); diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ConstraintTests.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ConstraintTests.java new file mode 100644 index 000000000..159c28035 --- /dev/null +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ConstraintTests.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.ballerina.openapi.generators.openapi; + +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * This test class for the covering the unit tests for constraint + * {@link io.ballerina.openapi.converter.service.ConstraintAnnotation} scenarios. + * + */ +public class ConstraintTests { + private static final Path RES_DIR = Paths.get("src/test/resources/ballerina-to-openapi").toAbsolutePath(); + + @Test(description = "When the record field has map type") + public void testMapFiled() throws IOException { + Path ballerinaFilePath = RES_DIR.resolve("constraint/record_field.bal"); + //Compare generated yaml file with expected yaml content + TestUtils.compareWithGeneratedFile(ballerinaFilePath, "data_type/map.yaml"); + } +} diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/constraint/record_field.bal b/openapi-cli/src/test/resources/ballerina-to-openapi/constraint/record_field.bal new file mode 100644 index 000000000..8a860b805 --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/constraint/record_field.bal @@ -0,0 +1,25 @@ +import ballerina/http; +import ballerina/constraint; + +@constraint:String {minLength: 5} +public type Address string; + +public type Person record { + @constraint:String {maxLength: 14} + string name?; + @constraint:Array {maxLength: 5, minLength: 2} + string[] hobby?; + @constraint:Int {maxValue: 5} + int id; + Address address?; + @constraint:Float {maxValue: 100000} + float salary?; + @constraint:Number {minValue: 500000} + decimal net?; +}; + +service /payloadV on new http:Listener(9090) { + resource function post pet(@http:Payload Person body) returns error? { + return; + } +} \ No newline at end of file diff --git a/openapi-cli/src/test/resources/testng.xml b/openapi-cli/src/test/resources/testng.xml index 2447d2f0e..85adc2b03 100644 --- a/openapi-cli/src/test/resources/testng.xml +++ b/openapi-cli/src/test/resources/testng.xml @@ -41,6 +41,7 @@ under the License. + From 9a7d8a3a03475e1bbc2e068394649841ae669db7 Mon Sep 17 00:00:00 2001 From: lnash94 Date: Mon, 25 Jul 2022 20:23:55 +0530 Subject: [PATCH 2/5] Constraint mapping for record fields --- .../service/ConstraintAnnotation.java | 31 ++-- .../service/OpenAPIComponentMapper.java | 135 +++++++++++++++--- .../service/OpenAPIParameterMapper.java | 2 +- .../service/OpenAPIQueryParameterMapper.java | 2 +- .../service/OpenAPIRequestBodyMapper.java | 8 +- .../service/OpenAPIResponseMapper.java | 5 +- .../generators/openapi/ConstraintTests.java | 2 +- .../expected_gen/constraint/record_field.yaml | 59 ++++++++ 8 files changed, 202 insertions(+), 42 deletions(-) create mode 100644 openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/record_field.yaml diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/ConstraintAnnotation.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/ConstraintAnnotation.java index b5076bf51..b8b403051 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/ConstraintAnnotation.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/ConstraintAnnotation.java @@ -17,6 +17,7 @@ */ package io.ballerina.openapi.converter.service; +import java.util.Optional; /** * @@ -41,32 +42,32 @@ public ConstraintAnnotation(ConstraintAnnotationBuilder builder) { this.maxValueExclusive = builder.maxValueExclusive; } - public String getMinValue() { - return minValue; + public Optional getMinValue() { + return Optional.ofNullable(minValue); } - public String getMaxValue() { - return maxValue; + public Optional getMaxValue() { + return Optional.ofNullable(maxValue); } - public String getLength() { - return length; + public Optional getLength() { + return Optional.ofNullable(length); } - public String getMinLength() { - return minLength; + public Optional getMinLength() { + return Optional.ofNullable(minLength); } - public String getMaxLength() { - return maxLength; + public Optional getMaxLength() { + return Optional.ofNullable(maxLength); } - public String getMinValueExclusive() { - return minValueExclusive; + public Optional getMinValueExclusive() { + return Optional.ofNullable(minValueExclusive); } - public String getMaxValueExclusive() { - return maxValueExclusive; + public Optional getMaxValueExclusive() { + return Optional.ofNullable(maxValueExclusive); } /** @@ -91,7 +92,7 @@ public ConstraintAnnotationBuilder withLength(String length) { return this; } - public ConstraintAnnotationBuilder setMaxValue(String maxValue) { + public ConstraintAnnotationBuilder withMaxValue(String maxValue) { this.maxValue = maxValue; return this; } diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java index 51fb63a61..f7faa5a3f 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java @@ -37,6 +37,19 @@ 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.syntax.tree.AnnotationNode; +import io.ballerina.compiler.syntax.tree.ExpressionNode; +import io.ballerina.compiler.syntax.tree.MappingConstructorExpressionNode; +import io.ballerina.compiler.syntax.tree.MappingFieldNode; +import io.ballerina.compiler.syntax.tree.MetadataNode; +import io.ballerina.compiler.syntax.tree.ModulePartNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.NonTerminalNode; +import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode; +import io.ballerina.compiler.syntax.tree.RecordFieldNode; +import io.ballerina.compiler.syntax.tree.SpecificFieldNode; +import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.openapi.converter.diagnostic.DiagnosticMessages; import io.ballerina.openapi.converter.diagnostic.IncompatibleResourceDiagnostic; import io.ballerina.openapi.converter.diagnostic.OpenAPIConverterDiagnostic; @@ -50,6 +63,7 @@ import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.media.StringSchema; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -58,6 +72,7 @@ import java.util.Locale; import java.util.Map; import java.util.Optional; +import java.util.Spliterator; import static io.ballerina.openapi.converter.Constants.DOUBLE; import static io.ballerina.openapi.converter.Constants.FLOAT; @@ -70,10 +85,12 @@ public class OpenAPIComponentMapper { private final Components components; private final List diagnostics; + private final NonTerminalNode rootNode; - public OpenAPIComponentMapper(Components components) { + public OpenAPIComponentMapper(Components components, NonTerminalNode rootNode) { this.components = components; + this.rootNode = rootNode; this.diagnostics = new ArrayList<>(); } @@ -266,24 +283,20 @@ private ObjectSchema generateObjectSchemaFromRecordFields(Map sc List required = new ArrayList<>(); componentSchema.setDescription(apiDocs.get(componentName)); Map schemaProperties = new LinkedHashMap<>(); - for (Map.Entry field: rfields.entrySet()) { - List annotations = field.getValue().annotations(); - ((RecordTypeSymbol)((TypeReferenceTypeSymbol)field.getValue().annotations().get(0).typeDescriptor().get()).typeDescriptor()).fieldDescriptors(); - annotations.stream().filter(annotSymbol->annotSymbol.getModule().get().getName().get().equals("constraint")) - .forEach(value-> { - ConstraintAnnotation.ConstraintAnnotationBuilder constraintAnnotBuilder = - new ConstraintAnnotation.ConstraintAnnotationBuilder(); - Map recordFields = - ((RecordTypeSymbol) ((TypeReferenceTypeSymbol) value.typeDescriptor() - .get()).typeDescriptor()).fieldDescriptors(); - recordFields.forEach((k,v) -> { - RecordFieldSymbol fieldSymbol = v; - switch (k) { - case "length": -// fieldSymbol. - } - }); - }); + for (Map.Entry field : rfields.entrySet()) { + ModulePartNode modulePartNode = rootNode.syntaxTree().rootNode(); + + RecordFieldNode fieldNode = (RecordFieldNode) modulePartNode. + findNode(field.getValue().getLocation().get().textRange()); + Optional metadata = fieldNode.metadata(); + ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder = + new ConstraintAnnotation.ConstraintAnnotationBuilder(); + + if (metadata.isPresent()) { + extractedConstraintAnnotation(metadata, constraintBuilder); + } + + ConstraintAnnotation constraintAnnot = constraintBuilder.build(); String fieldName = ConverterCommonUtils.unescapeIdentifier(field.getKey().trim()); if (!field.getValue().isOptional()) { @@ -317,6 +330,8 @@ private ObjectSchema generateObjectSchemaFromRecordFields(Map sc if (apiDocs.containsKey(fieldName)) { property.setDescription(apiDocs.get(fieldName)); } + // Assign value to property - string, number, array + setConstraintValueToSchema(constraintAnnot, property); schemaProperties.put(fieldName, property); } componentSchema.setProperties(schemaProperties); @@ -333,6 +348,88 @@ private ObjectSchema generateObjectSchemaFromRecordFields(Map sc return componentSchema; } + private void setConstraintValueToSchema(ConstraintAnnotation constraintAnnot, Schema property) { + + if (property instanceof ArraySchema) { + property.setMaxItems(constraintAnnot.getMaxLength().isPresent()? + Integer.valueOf(constraintAnnot.getMaxLength().get()) : null); + property.setMinItems(constraintAnnot.getMinLength().isPresent()? + Integer.valueOf(constraintAnnot.getMinLength().get()) : null); + } else { + property.setMaxLength(constraintAnnot.getMaxLength().isPresent()? + Integer.valueOf(constraintAnnot.getMaxLength().get()) : null); + property.setMinLength(constraintAnnot.getMinLength().isPresent()? + Integer.valueOf(constraintAnnot.getMinLength().get()) : null); + } + property.setMinimum(constraintAnnot.getMinValue().isPresent()? + BigDecimal.valueOf(Long.parseLong(constraintAnnot.getMinValue().get())): null); + + property.setMaximum(constraintAnnot.getMaxValue().isPresent()? + BigDecimal.valueOf(Long.parseLong(constraintAnnot.getMaxValue().get())): null); + } + + private void extractedConstraintAnnotation(Optional metadata, + ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder) { + + NodeList annotations = metadata.get().annotations(); + annotations.stream().filter(annot -> + (annot.annotReference() instanceof QualifiedNameReferenceNode && + ((QualifiedNameReferenceNode) annot.annotReference()).modulePrefix().text() + .equals("constraint"))) + .forEach(value -> { + + Optional fieldValues = value.annotValue(); + + if (fieldValues.isPresent()) { + Spliterator spliterator = fieldValues.get().fields().spliterator(); + spliterator.forEachRemaining(fieldV -> { + if (fieldV.kind() == SyntaxKind.SPECIFIC_FIELD) { + SpecificFieldNode specificFieldNode = (SpecificFieldNode) fieldV; + // generate string + String name = specificFieldNode.fieldName().toString().trim(); + if (specificFieldNode.valueExpr().isPresent()) { + ExpressionNode expressionNode = specificFieldNode.valueExpr().get(); + SyntaxKind kind = expressionNode.kind(); + if (kind == SyntaxKind.NUMERIC_LITERAL) { + String constraintValue = expressionNode.toString(); + fillConstraintValue(constraintBuilder, name, constraintValue); + } + } + } + }); + } + }); + } + + private void fillConstraintValue(ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder, + String name, String constraintValue) { + switch (name) { + case "minValue": + constraintBuilder.withMinValue(constraintValue); + break; + case "maxValue": + constraintBuilder.withMaxValue(constraintValue); + break; + case "minValueExclusive": + constraintBuilder.withMinValueExclusive(constraintValue); + break; + case "maxValueExclusive": + constraintBuilder.withMaxValueExclusive(constraintValue); + break; + case "length": + constraintBuilder.withLength(constraintValue); + break; + case "maxLength": + constraintBuilder.withMaxLength(constraintValue); + break; + case "minLength": + constraintBuilder.withMinLength(constraintValue); + break; + default: + break; + } + } + private Schema handleMapType(Map schema, String componentName, Schema property, MapTypeSymbol mapTypeSymbol) { diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIParameterMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIParameterMapper.java index 74214aa57..b8d302fc4 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIParameterMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIParameterMapper.java @@ -159,7 +159,7 @@ private void createPathParameters(List parameters, NodeList pat ResourcePathParameterNode pathParam = (ResourcePathParameterNode) param; if (pathParam.typeDescriptor().kind() == SyntaxKind.SIMPLE_NAME_REFERENCE) { SimpleNameReferenceNode queryNode = (SimpleNameReferenceNode) pathParam.typeDescriptor(); - OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components); + OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components, pathParam); TypeSymbol typeSymbol = (TypeSymbol) semanticModel.symbol(queryNode).orElseThrow(); componentMapper.createComponentSchema(components.getSchemas(), typeSymbol); Schema schema = new Schema(); diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIQueryParameterMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIQueryParameterMapper.java index 7a50e7602..e2e93d46a 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIQueryParameterMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIQueryParameterMapper.java @@ -112,7 +112,7 @@ public Parameter createQueryParameter(RequiredParameterNode queryParam) { QueryParameter queryParameter = new QueryParameter(); queryParameter.setName(ConverterCommonUtils.unescapeIdentifier(queryParamName)); SimpleNameReferenceNode queryNode = (SimpleNameReferenceNode) queryParam.typeName(); - OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components); + OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components, queryNode); TypeSymbol typeSymbol = (TypeSymbol) semanticModel.symbol(queryNode).orElseThrow(); componentMapper.createComponentSchema(components.getSchemas(), typeSymbol); Schema schema = new Schema(); diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIRequestBodyMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIRequestBodyMapper.java index 27f724bbe..201e23879 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIRequestBodyMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIRequestBodyMapper.java @@ -26,6 +26,7 @@ import io.ballerina.compiler.syntax.tree.MappingConstructorExpressionNode; import io.ballerina.compiler.syntax.tree.MappingFieldNode; import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NonTerminalNode; import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode; import io.ballerina.compiler.syntax.tree.RequiredParameterNode; import io.ballerina.compiler.syntax.tree.SeparatedNodeList; @@ -72,6 +73,7 @@ public class OpenAPIRequestBodyMapper { private final SemanticModel semanticModel; private final String customMediaType; private final List diagnostics; + private NonTerminalNode rootNode; /** * This constructor uses to create OpenAPIRequestBodyMapper instance when customMedia type enable. @@ -115,7 +117,7 @@ public List getDiagnostics() { */ public void handlePayloadAnnotation(RequiredParameterNode payloadNode, Map schema, AnnotationNode annotation, Map apiDocs) { - + rootNode = payloadNode; if ((annotation.annotReference().toString()).trim().equals(Constants.HTTP_PAYLOAD)) { // Creating request body - required. RequestBody bodyParameter = new RequestBody(); @@ -228,7 +230,7 @@ private void handleArrayTypePayload(Map schema, ArrayTypeDescrip //handle record for components SimpleNameReferenceNode referenceNode = (SimpleNameReferenceNode) typeDescriptorNode; TypeSymbol typeSymbol = getReferenceTypeSymbol(semanticModel.symbol(referenceNode)); - OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components); + OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components, referenceNode); componentMapper.createComponentSchema(schema, typeSymbol); diagnostics.addAll(componentMapper.getDiagnostics()); Schema itemSchema = new Schema(); @@ -283,7 +285,7 @@ private void createRequestBody(RequestBody bodyParameter, RequiredParameterNode private void handleReferencePayload(TypeSymbol typeSymbol, String recordName, Map schema, String mediaType, RequestBody bodyParameter) { //handle record for components - OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components); + OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components, rootNode); componentMapper.createComponentSchema(schema, typeSymbol); diagnostics.addAll(componentMapper.getDiagnostics()); io.swagger.v3.oas.models.media.MediaType media = new io.swagger.v3.oas.models.media.MediaType(); diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIResponseMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIResponseMapper.java index 0e8b327af..c9783a747 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIResponseMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIResponseMapper.java @@ -543,7 +543,8 @@ private Optional handleQualifiedNameType(ApiResponses apiResponses TypeSymbol typeSymbol = typeRef.typeDescriptor(); if (typeSymbol.typeKind() == TypeDescKind.RECORD) { ApiResponses responses = handleRecordTypeSymbol(qNode.identifier().text().trim(), - components.getSchemas(), customMediaPrefix, typeRef, new OpenAPIComponentMapper(components), + components.getSchemas(), customMediaPrefix, typeRef, + new OpenAPIComponentMapper(components, qNode), headers); apiResponses.putAll(responses); return Optional.of(apiResponses); @@ -795,7 +796,7 @@ private void handleReferenceResponse(OperationAdaptor operationAdaptor, SimpleNa Optional symbol = semanticModel.symbol(referenceNode); TypeSymbol typeSymbol = (TypeSymbol) symbol.orElseThrow(); //handle record for components - OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components); + OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components, referenceNode); String mediaTypeString; // Check typeInclusion is related to the http status code if (referenceNode.parent().kind().equals(ARRAY_TYPE_DESC)) { diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ConstraintTests.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ConstraintTests.java index 159c28035..769a6bc36 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ConstraintTests.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ConstraintTests.java @@ -35,6 +35,6 @@ public class ConstraintTests { public void testMapFiled() throws IOException { Path ballerinaFilePath = RES_DIR.resolve("constraint/record_field.bal"); //Compare generated yaml file with expected yaml content - TestUtils.compareWithGeneratedFile(ballerinaFilePath, "data_type/map.yaml"); + TestUtils.compareWithGeneratedFile(ballerinaFilePath, "constraint/record_field.yaml"); } } diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/record_field.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/record_field.yaml new file mode 100644 index 000000000..c7831c76d --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/record_field.yaml @@ -0,0 +1,59 @@ +openapi: 3.0.1 +info: + title: PayloadV + version: 0.0.0 +servers: + - url: "{server}:{port}/payloadV" + variables: + server: + default: http://localhost + port: + default: "9090" +paths: + /pet: + post: + operationId: postPet + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Person' + responses: + "500": + description: Found unexpected output + content: + text/plain: + schema: + type: string +components: + schemas: + Address: + type: string + Person: + required: + - id + type: object + properties: + name: + maxLength: 14 + type: string + hobby: + maxItems: 5 + minItems: 2 + type: array + items: + type: string + id: + maximum: 5 + type: integer + format: int64 + address: + $ref: '#/components/schemas/Address' + salary: + maximum: 100000 + type: number + format: float + net: + minimum: 500000 + type: number + format: double From 75a19b17caf40b1dfaa12407854214f6cd46ce7b Mon Sep 17 00:00:00 2001 From: lnash94 Date: Tue, 26 Jul 2022 19:23:24 +0530 Subject: [PATCH 3/5] Constraint mapping for array type --- .../diagnostic/DiagnosticMessages.java | 4 +- .../service/OpenAPIComponentMapper.java | 253 +++++++++++------- .../generators/openapi/ConstraintTests.java | 9 +- .../service/modules/types/types.bal | 3 + .../ballerina-to-openapi/constraint/array.bal | 33 +++ .../expected_gen/constraint/array.yaml | 74 +++++ .../expected_gen/constraint/record_field.yaml | 1 + 7 files changed, 279 insertions(+), 98 deletions(-) create mode 100644 openapi-cli/src/test/resources/ballerina-to-openapi/constraint/array.bal create mode 100644 openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/array.yaml diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/diagnostic/DiagnosticMessages.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/diagnostic/DiagnosticMessages.java index e45faafe6..4440862f7 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/diagnostic/DiagnosticMessages.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/diagnostic/DiagnosticMessages.java @@ -60,7 +60,9 @@ public enum DiagnosticMessages { " as it's not supported by the OpenAPI specification.", DiagnosticSeverity.WARNING), OAS_CONVERTOR_114("OAS_CONVERTOR_114", "Generated OpenAPI definition does not contain information " + - "for Ballerina type '%s'. ", DiagnosticSeverity.WARNING); + "for Ballerina type '%s'. ", DiagnosticSeverity.WARNING), + OAS_CONVERTOR_115("OAS_CONVERTOR_115", "Failed to parser the Number value due to: %s ", + DiagnosticSeverity.ERROR); private final String code; private final String description; diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java index f7faa5a3f..9404d9b2b 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java @@ -18,7 +18,6 @@ package io.ballerina.openapi.converter.service; -import io.ballerina.compiler.api.symbols.AnnotationSymbol; import io.ballerina.compiler.api.symbols.ArrayTypeSymbol; import io.ballerina.compiler.api.symbols.ConstantSymbol; import io.ballerina.compiler.api.symbols.Documentable; @@ -43,14 +42,15 @@ import io.ballerina.compiler.syntax.tree.MappingFieldNode; import io.ballerina.compiler.syntax.tree.MetadataNode; import io.ballerina.compiler.syntax.tree.ModulePartNode; -import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.NodeList; import io.ballerina.compiler.syntax.tree.NonTerminalNode; import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode; import io.ballerina.compiler.syntax.tree.RecordFieldNode; import io.ballerina.compiler.syntax.tree.SpecificFieldNode; import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.compiler.syntax.tree.TypeDefinitionNode; import io.ballerina.openapi.converter.diagnostic.DiagnosticMessages; +import io.ballerina.openapi.converter.diagnostic.ExceptionDiagnostic; import io.ballerina.openapi.converter.diagnostic.IncompatibleResourceDiagnostic; import io.ballerina.openapi.converter.diagnostic.OpenAPIConverterDiagnostic; import io.ballerina.openapi.converter.utils.ConverterCommonUtils; @@ -64,6 +64,8 @@ import io.swagger.v3.oas.models.media.StringSchema; import java.math.BigDecimal; +import java.text.NumberFormat; +import java.text.ParseException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -88,6 +90,7 @@ public class OpenAPIComponentMapper { private final NonTerminalNode rootNode; + public OpenAPIComponentMapper(Components components, NonTerminalNode rootNode) { this.components = components; this.rootNode = rootNode; @@ -126,31 +129,51 @@ public void createComponentSchema(Map schema, TypeSymbol typeSym } } } + //Access module part node for finding the node to given typeSymbol. + //TODO: this works for module reference. + ModulePartNode modulePartNode = rootNode.syntaxTree().rootNode(); + NonTerminalNode nonTerminalNode = modulePartNode.findNode(typeSymbol.getLocation().get().textRange()); + ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder = + new ConstraintAnnotation.ConstraintAnnotationBuilder(); + if (nonTerminalNode instanceof TypeDefinitionNode) { + TypeDefinitionNode node = (TypeDefinitionNode) nonTerminalNode; + + if (node.metadata().isPresent()) { + extractedConstraintAnnotation(node.metadata().get(), constraintBuilder); + } + } + ConstraintAnnotation constraintAnnot = constraintBuilder.build(); + switch (type.typeKind()) { case RECORD: // Handle typeInclusions with allOf type binding handleRecordTypeSymbol((RecordTypeSymbol) type, schema, componentName, apiDocs); break; case STRING: - schema.put(componentName, new StringSchema().description(typeDoc)); + schema.put(componentName, setConstraintValueToSchema(constraintAnnot, + new StringSchema().description(typeDoc))); components.setSchemas(schema); break; case INT: - schema.put(componentName, new IntegerSchema().description(typeDoc)); + schema.put(componentName, setConstraintValueToSchema(constraintAnnot, + new IntegerSchema().description(typeDoc))); components.setSchemas(schema); break; case DECIMAL: - schema.put(componentName, new NumberSchema().format(DOUBLE).description(typeDoc)); + schema.put(componentName, setConstraintValueToSchema(constraintAnnot, + new NumberSchema().format(DOUBLE).description(typeDoc))); components.setSchemas(schema); break; case FLOAT: - schema.put(componentName, new NumberSchema().format(FLOAT).description(typeDoc)); + schema.put(componentName, setConstraintValueToSchema(constraintAnnot, + new NumberSchema().format(FLOAT).description(typeDoc))); components.setSchemas(schema); break; case ARRAY: case TUPLE: ArraySchema arraySchema = mapArrayToArraySchema(schema, type, componentName); - schema.put(componentName, arraySchema.description(typeDoc)); + schema.put(componentName, setConstraintValueToSchema(constraintAnnot, + arraySchema.description(typeDoc))); components.setSchemas(schema); break; case UNION: @@ -284,16 +307,18 @@ private ObjectSchema generateObjectSchemaFromRecordFields(Map sc componentSchema.setDescription(apiDocs.get(componentName)); Map schemaProperties = new LinkedHashMap<>(); for (Map.Entry field : rfields.entrySet()) { - ModulePartNode modulePartNode = rootNode.syntaxTree().rootNode(); - - RecordFieldNode fieldNode = (RecordFieldNode) modulePartNode. - findNode(field.getValue().getLocation().get().textRange()); - Optional metadata = fieldNode.metadata(); ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder = new ConstraintAnnotation.ConstraintAnnotationBuilder(); - if (metadata.isPresent()) { - extractedConstraintAnnotation(metadata, constraintBuilder); + if (!(rootNode instanceof QualifiedNameReferenceNode)) { + ModulePartNode modulePartNode = rootNode.syntaxTree().rootNode(); + NonTerminalNode node = modulePartNode.findNode(field.getValue().getLocation().get().textRange()); + if (node instanceof RecordFieldNode) { + RecordFieldNode fieldNode = (RecordFieldNode) modulePartNode. + findNode(field.getValue().getLocation().get().textRange()); + Optional metadata = fieldNode.metadata(); + metadata.ifPresent(metadataNode -> extractedConstraintAnnotation(metadataNode, constraintBuilder)); + } } ConstraintAnnotation constraintAnnot = constraintBuilder.build(); @@ -348,88 +373,6 @@ private ObjectSchema generateObjectSchemaFromRecordFields(Map sc return componentSchema; } - private void setConstraintValueToSchema(ConstraintAnnotation constraintAnnot, Schema property) { - - if (property instanceof ArraySchema) { - property.setMaxItems(constraintAnnot.getMaxLength().isPresent()? - Integer.valueOf(constraintAnnot.getMaxLength().get()) : null); - property.setMinItems(constraintAnnot.getMinLength().isPresent()? - Integer.valueOf(constraintAnnot.getMinLength().get()) : null); - } else { - property.setMaxLength(constraintAnnot.getMaxLength().isPresent()? - Integer.valueOf(constraintAnnot.getMaxLength().get()) : null); - property.setMinLength(constraintAnnot.getMinLength().isPresent()? - Integer.valueOf(constraintAnnot.getMinLength().get()) : null); - } - property.setMinimum(constraintAnnot.getMinValue().isPresent()? - BigDecimal.valueOf(Long.parseLong(constraintAnnot.getMinValue().get())): null); - - property.setMaximum(constraintAnnot.getMaxValue().isPresent()? - BigDecimal.valueOf(Long.parseLong(constraintAnnot.getMaxValue().get())): null); - } - - private void extractedConstraintAnnotation(Optional metadata, - ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder) { - - NodeList annotations = metadata.get().annotations(); - annotations.stream().filter(annot -> - (annot.annotReference() instanceof QualifiedNameReferenceNode && - ((QualifiedNameReferenceNode) annot.annotReference()).modulePrefix().text() - .equals("constraint"))) - .forEach(value -> { - - Optional fieldValues = value.annotValue(); - - if (fieldValues.isPresent()) { - Spliterator spliterator = fieldValues.get().fields().spliterator(); - spliterator.forEachRemaining(fieldV -> { - if (fieldV.kind() == SyntaxKind.SPECIFIC_FIELD) { - SpecificFieldNode specificFieldNode = (SpecificFieldNode) fieldV; - // generate string - String name = specificFieldNode.fieldName().toString().trim(); - if (specificFieldNode.valueExpr().isPresent()) { - ExpressionNode expressionNode = specificFieldNode.valueExpr().get(); - SyntaxKind kind = expressionNode.kind(); - if (kind == SyntaxKind.NUMERIC_LITERAL) { - String constraintValue = expressionNode.toString(); - fillConstraintValue(constraintBuilder, name, constraintValue); - } - } - } - }); - } - }); - } - - private void fillConstraintValue(ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder, - String name, String constraintValue) { - switch (name) { - case "minValue": - constraintBuilder.withMinValue(constraintValue); - break; - case "maxValue": - constraintBuilder.withMaxValue(constraintValue); - break; - case "minValueExclusive": - constraintBuilder.withMinValueExclusive(constraintValue); - break; - case "maxValueExclusive": - constraintBuilder.withMaxValueExclusive(constraintValue); - break; - case "length": - constraintBuilder.withLength(constraintValue); - break; - case "maxLength": - constraintBuilder.withMaxLength(constraintValue); - break; - case "minLength": - constraintBuilder.withMinLength(constraintValue); - break; - default: - break; - } - } - private Schema handleMapType(Map schema, String componentName, Schema property, MapTypeSymbol mapTypeSymbol) { @@ -680,4 +623,122 @@ private ArraySchema handleArray(int arrayDimensions, Schema property, ArraySchem return arrayProperty; } + /** + * This util uses to set the constraint value for relevant schema field. + */ + private Schema setConstraintValueToSchema(ConstraintAnnotation constraintAnnot, Schema property) { + + if (property instanceof ArraySchema) { + property.setMaxItems(constraintAnnot.getMaxLength().isPresent() ? + Integer.valueOf(constraintAnnot.getMaxLength().get()) : null); + property.setMinItems(constraintAnnot.getMinLength().isPresent() ? + Integer.valueOf(constraintAnnot.getMinLength().get()) : null); + } else { + property.setMaxLength(constraintAnnot.getMaxLength().isPresent() ? + Integer.valueOf(constraintAnnot.getMaxLength().get()) : null); + property.setMinLength(constraintAnnot.getMinLength().isPresent() ? + Integer.valueOf(constraintAnnot.getMinLength().get()) : null); + } + + try { + + BigDecimal minimum = null; + BigDecimal maximum = null; + if (constraintAnnot.getMinValue().isPresent()) { + try { + minimum = BigDecimal.valueOf(Integer.parseInt(constraintAnnot.getMinValue().get())); + } catch (NumberFormatException e) { + minimum = BigDecimal.valueOf(NumberFormat.getInstance().parse( + constraintAnnot.getMinValue().get()).doubleValue()); + } + } + + if (constraintAnnot.getMaxValue().isPresent()) { + try { + maximum = BigDecimal.valueOf(Integer.parseInt(constraintAnnot.getMaxValue().get())); + } catch (NumberFormatException e) { + maximum = BigDecimal.valueOf((NumberFormat.getInstance() + .parse(constraintAnnot.getMaxValue().get()).doubleValue())); + } + + } + property.setMinimum(minimum); + property.setMaximum(maximum); + + } catch (ParseException parserMessage) { + DiagnosticMessages error = DiagnosticMessages.OAS_CONVERTOR_110; + ExceptionDiagnostic diagnostic = new ExceptionDiagnostic(error.getCode(), + error.getDescription(), null, parserMessage.getMessage()); + diagnostics.add(diagnostic); + } + + return property; + } + + /** + * This util uses to extract the annotation values in `@constraint` and store it in builder. + */ + private void extractedConstraintAnnotation(MetadataNode metadata, + ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder) { + + NodeList annotations = metadata.annotations(); + annotations.stream().filter(annot -> + (annot.annotReference() instanceof QualifiedNameReferenceNode && + ((QualifiedNameReferenceNode) annot.annotReference()).modulePrefix().text() + .equals("constraint"))) + .forEach(value -> { + + Optional fieldValues = value.annotValue(); + if (fieldValues.isPresent()) { + Spliterator spliterator = fieldValues.get().fields().spliterator(); + spliterator.forEachRemaining(fieldV -> { + if (fieldV.kind() == SyntaxKind.SPECIFIC_FIELD) { + SpecificFieldNode specificFieldNode = (SpecificFieldNode) fieldV; + // generate string + String name = specificFieldNode.fieldName().toString().trim(); + if (specificFieldNode.valueExpr().isPresent()) { + ExpressionNode expressionNode = specificFieldNode.valueExpr().get(); + SyntaxKind kind = expressionNode.kind(); + if (kind == SyntaxKind.NUMERIC_LITERAL) { + String constraintValue = expressionNode.toString(); + fillConstraintValue(constraintBuilder, name, constraintValue); + } + } + } + }); + } + }); + } + + /** + * This util uses to build the constraint builder with available constraint annotation field value. + */ + private void fillConstraintValue(ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder, + String name, String constraintValue) { + switch (name) { + case "minValue": + constraintBuilder.withMinValue(constraintValue); + break; + case "maxValue": + constraintBuilder.withMaxValue(constraintValue); + break; + case "minValueExclusive": + constraintBuilder.withMinValueExclusive(constraintValue); + break; + case "maxValueExclusive": + constraintBuilder.withMaxValueExclusive(constraintValue); + break; + case "length": + constraintBuilder.withLength(constraintValue); + break; + case "maxLength": + constraintBuilder.withMaxLength(constraintValue); + break; + case "minLength": + constraintBuilder.withMinLength(constraintValue); + break; + default: + break; + } + } } diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ConstraintTests.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ConstraintTests.java index 769a6bc36..874b0f3eb 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ConstraintTests.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ConstraintTests.java @@ -31,10 +31,17 @@ public class ConstraintTests { private static final Path RES_DIR = Paths.get("src/test/resources/ballerina-to-openapi").toAbsolutePath(); - @Test(description = "When the record field has map type") + @Test(description = "When the record field has constraint type") public void testMapFiled() throws IOException { Path ballerinaFilePath = RES_DIR.resolve("constraint/record_field.bal"); //Compare generated yaml file with expected yaml content TestUtils.compareWithGeneratedFile(ballerinaFilePath, "constraint/record_field.yaml"); } + + @Test(description = "When the record field has array type") + public void testArrayType() throws IOException { + Path ballerinaFilePath = RES_DIR.resolve("constraint/array.bal"); + //Compare generated yaml file with expected yaml content + TestUtils.compareWithGeneratedFile(ballerinaFilePath, "constraint/array.yaml"); + } } diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/ballerina-project/service/modules/types/types.bal b/openapi-cli/src/test/resources/ballerina-to-openapi/ballerina-project/service/modules/types/types.bal index 4b47d1088..ae2012381 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/ballerina-project/service/modules/types/types.bal +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/ballerina-project/service/modules/types/types.bal @@ -1,4 +1,6 @@ import ballerina/http; +import ballerina/constraint; + # Represents a product # # + name - Name of the product @@ -6,6 +8,7 @@ import ballerina/http; # + price - Product price public type Product record {| string id?; + @constraint:String {maxLength: 14} string name; string description; Price price; diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/constraint/array.bal b/openapi-cli/src/test/resources/ballerina-to-openapi/constraint/array.bal new file mode 100644 index 000000000..d1c8c121e --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/constraint/array.bal @@ -0,0 +1,33 @@ +import ballerina/http; +import ballerina/constraint; + +@constraint:String {maxLength: 23} +public type HobbyItemsString string; + +@constraint:String {minLength: 7} +public type PersonDetailsItemsString string; + +@constraint:Float {maxValue: 445.4} +public type PersonFeeItemsNumber float; + +@constraint:Int {maxValue: 67 } +public type PersonLimitItemsInteger int; + +@constraint:Array {maxLength: 5, minLength: 2} +public type Hobby HobbyItemsString[]; + +public type Person record { + Hobby hobby?; + @constraint:Array {maxLength: 5} + PersonDetailsItemsString[] Details?; + int id; + PersonFeeItemsNumber[] fee?; + # The maximum number of items in the response (as set in the query or by default). + PersonLimitItemsInteger[] 'limit?; +}; + +service /payloadV on new http:Listener(9090) { + resource function post pet(@http:Payload Person body) returns error? { + return; + } +} \ No newline at end of file diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/array.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/array.yaml new file mode 100644 index 000000000..b9b20e03b --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/array.yaml @@ -0,0 +1,74 @@ +openapi: 3.0.1 +info: + title: PayloadV + version: 0.0.0 +servers: + - url: "{server}:{port}/payloadV" + variables: + server: + default: http://localhost + port: + default: "9090" +paths: + /pet: + post: + operationId: postPet + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Person' + responses: + "500": + description: Found unexpected output + content: + text/plain: + schema: + type: string +components: + schemas: + PersonDetailsItemsString: + minLength: 7 + type: string + PersonFeeItemsNumber: + maximum: 445.4 + type: number + format: float + PersonLimitItemsInteger: + maximum: 67 + type: integer + format: int32 + Hobby: + maxItems: 5 + minItems: 2 + type: array + items: + $ref: '#/components/schemas/HobbyItemsString' + HobbyItemsString: + maxLength: 23 + type: string + Person: + required: + - id + type: object + properties: + hobby: + $ref: '#/components/schemas/Hobby' + Details: + maxItems: 5 + type: array + items: + $ref: '#/components/schemas/PersonDetailsItemsString' + id: + type: integer + format: int64 + fee: + type: array + items: + $ref: '#/components/schemas/PersonFeeItemsNumber' + limit: + type: array + description: The maximum number of items in the response (as set in the + query or by default). + items: + $ref: '#/components/schemas/PersonLimitItemsInteger' diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/record_field.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/record_field.yaml index c7831c76d..25ffb4b6e 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/record_field.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/record_field.yaml @@ -28,6 +28,7 @@ paths: components: schemas: Address: + minLength: 5 type: string Person: required: From e88663218757e2662a9389f66bda5248f16f34ab Mon Sep 17 00:00:00 2001 From: lnash94 Date: Tue, 22 Aug 2023 15:54:18 +0530 Subject: [PATCH 4/5] Resolve conflict and apply project API changes --- .../diagnostic/DiagnosticMessages.java | 6 +- .../service/ConstraintAnnotation.java | 7 +- .../service/ModuleMemberVisitor.java | 11 ++- .../service/OpenAPIComponentMapper.java | 73 ++++++++++++------- .../service/OpenAPIHeaderMapper.java | 11 ++- .../service/OpenAPIParameterMapper.java | 28 ++++--- .../service/OpenAPIQueryParameterMapper.java | 30 ++++---- .../service/OpenAPIRequestBodyMapper.java | 16 ++-- .../service/OpenAPIResourceMapper.java | 8 +- .../service/OpenAPIResponseMapper.java | 9 ++- .../service/OpenAPIServiceMapper.java | 6 +- .../converter/utils/ConverterCommonUtils.java | 5 +- .../utils/ServiceToOpenAPIConverterUtils.java | 12 +-- .../generators/openapi/ConstraintTests.java | 24 +++--- .../openapi/ModuleReferenceTests.java | 4 +- .../expected_gen/arrayTypeResponse.yaml | 1 + .../expected_gen/constraint/array.yaml | 30 +++++++- .../expected_gen/constraint/record_field.yaml | 28 ++++++- 18 files changed, 197 insertions(+), 112 deletions(-) diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/diagnostic/DiagnosticMessages.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/diagnostic/DiagnosticMessages.java index 4979795fe..a09dbeca0 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/diagnostic/DiagnosticMessages.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/diagnostic/DiagnosticMessages.java @@ -61,11 +61,9 @@ public enum DiagnosticMessages { DiagnosticSeverity.WARNING), OAS_CONVERTOR_114("OAS_CONVERTOR_114", "Generated OpenAPI definition does not contain information " + "for Ballerina type '%s'. ", DiagnosticSeverity.WARNING), - OAS_CONVERTOR_115("OAS_CONVERTOR_115", "Failed to parser the Number value due to: %s ", - DiagnosticSeverity.ERROR), - "for Ballerina type '%s'. ", DiagnosticSeverity.WARNING), -//todo resolve conflicts OAS_CONVERTOR_115("OAS_CONVERTOR_115", "Given Ballerina file does not contain any HTTP service.", + DiagnosticSeverity.ERROR), + OAS_CONVERTOR_116("OAS_CONVERTOR_116", "Failed to parser the Number value due to: %s ", DiagnosticSeverity.ERROR); private final String code; diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/ConstraintAnnotation.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/ConstraintAnnotation.java index b8b403051..44121cd2a 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/ConstraintAnnotation.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/ConstraintAnnotation.java @@ -1,7 +1,7 @@ /* - * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2022, WSO2 LLC. (https://www.wso2.com). * - * WSO2 Inc. licenses this file to you under the Apache License, + * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except * in compliance with the License. * You may obtain a copy of the License at @@ -20,8 +20,9 @@ import java.util.Optional; /** + * This @link ConstraintAnnotation} class represents the constraint annotations. * - * @since 1.2.0 + * @since 1.9.0 */ public class ConstraintAnnotation { private final String minValue; diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/ModuleMemberVisitor.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/ModuleMemberVisitor.java index 252325a26..565d21b3d 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/ModuleMemberVisitor.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/ModuleMemberVisitor.java @@ -22,8 +22,7 @@ import io.ballerina.compiler.syntax.tree.NodeVisitor; import io.ballerina.compiler.syntax.tree.TypeDefinitionNode; -import java.util.LinkedList; -import java.util.List; +import java.util.LinkedHashSet; /** * Visitor to get the TypeDefinitionNode and ListenerDeclarationNodes. @@ -32,8 +31,8 @@ */ public class ModuleMemberVisitor extends NodeVisitor { - LinkedList typeDefinitionNodes = new LinkedList<>(); - LinkedList listenerDeclarationNodes = new LinkedList<>(); + LinkedHashSet typeDefinitionNodes = new LinkedHashSet<>(); + LinkedHashSet listenerDeclarationNodes = new LinkedHashSet<>(); @Override public void visit(TypeDefinitionNode typeDefinitionNode) { @@ -45,11 +44,11 @@ public void visit(ListenerDeclarationNode listenerDeclarationNode) { listenerDeclarationNodes.add(listenerDeclarationNode); } - public List getTypeDefinitionNodes() { + public LinkedHashSet getTypeDefinitionNodes() { return typeDefinitionNodes; } - public List getListenerDeclarationNodes() { + public LinkedHashSet getListenerDeclarationNodes() { return listenerDeclarationNodes; } } diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java index 32c5e155a..a4c9b6f26 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java @@ -38,14 +38,15 @@ import io.ballerina.compiler.api.symbols.UnionTypeSymbol; import io.ballerina.compiler.syntax.tree.AnnotationNode; import io.ballerina.compiler.syntax.tree.ExpressionNode; +import io.ballerina.compiler.syntax.tree.IntersectionTypeDescriptorNode; import io.ballerina.compiler.syntax.tree.MappingConstructorExpressionNode; import io.ballerina.compiler.syntax.tree.MappingFieldNode; import io.ballerina.compiler.syntax.tree.MetadataNode; -import io.ballerina.compiler.syntax.tree.ModulePartNode; +import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.NodeList; -import io.ballerina.compiler.syntax.tree.NonTerminalNode; import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode; import io.ballerina.compiler.syntax.tree.RecordFieldNode; +import io.ballerina.compiler.syntax.tree.RecordTypeDescriptorNode; import io.ballerina.compiler.syntax.tree.SpecificFieldNode; import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.compiler.syntax.tree.TypeDefinitionNode; @@ -70,6 +71,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; @@ -89,14 +91,13 @@ public class OpenAPIComponentMapper { private final Components components; private final List diagnostics; - private final NonTerminalNode rootNode; private final HashSet visitedTypeDefinitionNames = new HashSet<>(); + private final LinkedHashSet typeDefinitionNodes; - - public OpenAPIComponentMapper(Components components, NonTerminalNode rootNode) { + public OpenAPIComponentMapper(Components components, ModuleMemberVisitor moduleMemberVisitor) { this.components = components; - this.rootNode = rootNode; this.diagnostics = new ArrayList<>(); + this.typeDefinitionNodes = moduleMemberVisitor.getTypeDefinitionNodes(); } public List getDiagnostics() { @@ -122,24 +123,22 @@ public void createComponentSchema(Map schema, TypeSymbol typeSym } TypeReferenceTypeSymbol typeRef = (TypeReferenceTypeSymbol) typeSymbol; TypeSymbol type = typeRef.typeDescriptor(); - // Handle record type request body if (type.typeKind() == TypeDescKind.INTERSECTION) { type = excludeReadonlyIfPresent(type); } //Access module part node for finding the node to given typeSymbol. - //TODO: this works for module reference. - ModulePartNode modulePartNode = rootNode.syntaxTree().rootNode(); - NonTerminalNode nonTerminalNode = modulePartNode.findNode(typeSymbol.getLocation().get().textRange()); ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder = new ConstraintAnnotation.ConstraintAnnotationBuilder(); - if (nonTerminalNode instanceof TypeDefinitionNode) { - TypeDefinitionNode node = (TypeDefinitionNode) nonTerminalNode; - - if (node.metadata().isPresent()) { - extractedConstraintAnnotation(node.metadata().get(), constraintBuilder); + ((TypeReferenceTypeSymbol) typeSymbol).definition().getName().ifPresent(name -> { + for (TypeDefinitionNode typeDefinitionNode : typeDefinitionNodes) { + if (typeDefinitionNode.typeName().text().equals(name)) { + if (typeDefinitionNode.metadata().isPresent()) { + extractedConstraintAnnotation(typeDefinitionNode.metadata().get(), constraintBuilder); + } + } } - } + }); ConstraintAnnotation constraintAnnot = constraintBuilder.build(); switch (type.typeKind()) { @@ -366,23 +365,43 @@ private ObjectSchema generateObjectSchemaFromRecordFields(Map sc List required = new ArrayList<>(); componentSchema.setDescription(apiDocs.get(componentName)); Map schemaProperties = new LinkedHashMap<>(); + RecordTypeDescriptorNode record = null; + // Get the record type descriptor node from given record type symbol + for (TypeDefinitionNode typeDefinitionNode : typeDefinitionNodes) { + if (typeDefinitionNode.typeName().text().equals(componentName)) { + if (typeDefinitionNode.typeDescriptor().kind().equals(SyntaxKind.RECORD_TYPE_DESC)) { + record = (RecordTypeDescriptorNode) typeDefinitionNode.typeDescriptor(); + } else if (typeDefinitionNode.typeDescriptor().kind().equals(SyntaxKind.INTERSECTION_TYPE_DESC)) { + IntersectionTypeDescriptorNode intersecNode = + (IntersectionTypeDescriptorNode) typeDefinitionNode.typeDescriptor(); + Node leftTypeDesc = intersecNode.leftTypeDesc(); + Node rightTypeDesc = intersecNode.rightTypeDesc(); + if (leftTypeDesc.kind().equals(SyntaxKind.RECORD_TYPE_DESC)) { + record = (RecordTypeDescriptorNode) leftTypeDesc; + } + if (rightTypeDesc.kind().equals(SyntaxKind.RECORD_TYPE_DESC)) { + record = (RecordTypeDescriptorNode) rightTypeDesc; + } + } + } + } + for (Map.Entry field : rfields.entrySet()) { ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder = new ConstraintAnnotation.ConstraintAnnotationBuilder(); - - if (!(rootNode instanceof QualifiedNameReferenceNode)) { - ModulePartNode modulePartNode = rootNode.syntaxTree().rootNode(); - NonTerminalNode node = modulePartNode.findNode(field.getValue().getLocation().get().textRange()); - if (node instanceof RecordFieldNode) { - RecordFieldNode fieldNode = (RecordFieldNode) modulePartNode. - findNode(field.getValue().getLocation().get().textRange()); - Optional metadata = fieldNode.metadata(); - metadata.ifPresent(metadataNode -> extractedConstraintAnnotation(metadataNode, constraintBuilder)); + if (record != null) { + for (Node node : record.fields()) { + if (node instanceof RecordFieldNode) { + RecordFieldNode fieldNode = (RecordFieldNode) node; + if (fieldNode.fieldName().toString().equals(field.getKey())) { + Optional metadata = fieldNode.metadata(); + metadata.ifPresent(metadataNode -> extractedConstraintAnnotation(metadataNode, + constraintBuilder)); + } + } } } - ConstraintAnnotation constraintAnnot = constraintBuilder.build(); - String fieldName = ConverterCommonUtils.unescapeIdentifier(field.getKey().trim()); if (!field.getValue().isOptional()) { required.add(fieldName); diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIHeaderMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIHeaderMapper.java index 70466d441..054859d71 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIHeaderMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIHeaderMapper.java @@ -59,11 +59,14 @@ public class OpenAPIHeaderMapper { private final Components components; private final SemanticModel semanticModel; private final Map apidocs; + private final ModuleMemberVisitor moduleMemberVisitor; - public OpenAPIHeaderMapper(Components components, SemanticModel semanticModel, Map apidocs) { + public OpenAPIHeaderMapper(Components components, SemanticModel semanticModel, Map apidocs, + ModuleMemberVisitor moduleMemberVisitor) { this.apidocs = apidocs; this.components = components; this.semanticModel = semanticModel; + this.moduleMemberVisitor = moduleMemberVisitor; } /** @@ -86,7 +89,7 @@ public List setHeaderParameter(RequiredParameterNode headerParam) { if (headerDetailNode.kind() == SyntaxKind.SIMPLE_NAME_REFERENCE) { SimpleNameReferenceNode refNode = (SimpleNameReferenceNode) headerDetailNode; - headerTypeSchema = handleReference(semanticModel, components, refNode); + headerTypeSchema = handleReference(semanticModel, components, refNode, moduleMemberVisitor); } else { headerTypeSchema = ConverterCommonUtils.getOpenApiSchema(getHeaderType(headerParam)); } @@ -126,7 +129,7 @@ public List setHeaderParameter(DefaultableParameterNode headerParam) Schema headerTypeSchema; if (headerParam.typeName().kind() == SyntaxKind.SIMPLE_NAME_REFERENCE) { SimpleNameReferenceNode refNode = (SimpleNameReferenceNode) headerParam.typeName(); - headerTypeSchema = handleReference(semanticModel, components, refNode); + headerTypeSchema = handleReference(semanticModel, components, refNode, moduleMemberVisitor); } else { headerTypeSchema = ConverterCommonUtils.getOpenApiSchema(getHeaderType(headerParam)); } @@ -184,7 +187,7 @@ private void completeHeaderParameter(List parameters, String headerNa Schema itemSchema; if (kind == SyntaxKind.SIMPLE_NAME_REFERENCE) { SimpleNameReferenceNode refNode = (SimpleNameReferenceNode) arrayNode.memberTypeDesc(); - itemSchema = handleReference(semanticModel, components, refNode); + itemSchema = handleReference(semanticModel, components, refNode, moduleMemberVisitor); } else { itemSchema = ConverterCommonUtils.getOpenApiSchema(kind); } diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIParameterMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIParameterMapper.java index 6b137ea9b..cfb686174 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIParameterMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIParameterMapper.java @@ -58,6 +58,7 @@ import static io.ballerina.openapi.converter.Constants.WILD_CARD_CONTENT_KEY; import static io.ballerina.openapi.converter.Constants.WILD_CARD_SUMMARY; import static io.ballerina.openapi.converter.utils.ConverterCommonUtils.extractCustomMediaType; +import static io.ballerina.openapi.converter.utils.ConverterCommonUtils.unescapeIdentifier; /** * OpenAPIParameterMapper provides functionality for converting ballerina parameter to OAS parameter model. @@ -69,6 +70,7 @@ public class OpenAPIParameterMapper { private final List errors = new ArrayList<>(); private final Components components; private final SemanticModel semanticModel; + private final ModuleMemberVisitor moduleMemberVisitor; public List getErrors() { return errors; @@ -76,13 +78,15 @@ public List getErrors() { public OpenAPIParameterMapper(FunctionDefinitionNode functionDefinitionNode, OperationAdaptor operationAdaptor, Map apidocs, - Components components, SemanticModel semanticModel) { + Components components, SemanticModel semanticModel, + ModuleMemberVisitor moduleMemberVisitor) { this.functionDefinitionNode = functionDefinitionNode; this.operationAdaptor = operationAdaptor; this.apidocs = apidocs; this.components = components; this.semanticModel = semanticModel; + this.moduleMemberVisitor = moduleMemberVisitor; } @@ -100,7 +104,7 @@ public void getResourceInputs(Components components, SemanticModel semanticModel SeparatedNodeList parameterList = functionSignature.parameters(); for (ParameterNode parameterNode : parameterList) { OpenAPIQueryParameterMapper queryParameterMapper = new OpenAPIQueryParameterMapper(apidocs, components, - semanticModel); + semanticModel, moduleMemberVisitor); if (parameterNode.kind() == SyntaxKind.REQUIRED_PARAM) { RequiredParameterNode requiredParameterNode = (RequiredParameterNode) parameterNode; // Handle query parameter @@ -159,11 +163,12 @@ private void createPathParameters(List parameters, NodeList pat ResourcePathParameterNode pathParam = (ResourcePathParameterNode) param; if (pathParam.typeDescriptor().kind() == SyntaxKind.SIMPLE_NAME_REFERENCE) { SimpleNameReferenceNode queryNode = (SimpleNameReferenceNode) pathParam.typeDescriptor(); - OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components, pathParam); + OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components, + moduleMemberVisitor); TypeSymbol typeSymbol = (TypeSymbol) semanticModel.symbol(queryNode).orElseThrow(); componentMapper.createComponentSchema(components.getSchemas(), typeSymbol); Schema schema = new Schema(); - schema.set$ref(ConverterCommonUtils.unescapeIdentifier(queryNode.name().text().trim())); + schema.set$ref(unescapeIdentifier(queryNode.name().text().trim())); pathParameterOAS.setSchema(schema); } else { pathParameterOAS.schema(ConverterCommonUtils.getOpenApiSchema( @@ -195,12 +200,13 @@ private void handleAnnotationParameters(Components components, for (AnnotationNode annotation: annotations) { if ((annotation.annotReference().toString()).trim().equals(Constants.HTTP_HEADER)) { // Handle headers. - OpenAPIHeaderMapper openAPIHeaderMapper = new OpenAPIHeaderMapper(components, semanticModel, apidocs); + OpenAPIHeaderMapper openAPIHeaderMapper = new OpenAPIHeaderMapper(components, semanticModel, apidocs, + moduleMemberVisitor); parameters.addAll(openAPIHeaderMapper.setHeaderParameter(requiredParameterNode)); } else if ((annotation.annotReference().toString()).trim().equals(Constants.HTTP_QUERY)) { // Handle query parameter. OpenAPIQueryParameterMapper openAPIQueryParameterMapper = new OpenAPIQueryParameterMapper(apidocs, - components, semanticModel); + components, semanticModel, moduleMemberVisitor); parameters.add(openAPIQueryParameterMapper.createQueryParameter(requiredParameterNode)); } else if ((annotation.annotReference().toString()).trim().equals(Constants.HTTP_PAYLOAD) && (!Constants.GET.toLowerCase(Locale.ENGLISH).equalsIgnoreCase( @@ -210,8 +216,9 @@ private void handleAnnotationParameters(Components components, Optional customMediaType = extractCustomMediaType(functionDefinitionNode); OpenAPIRequestBodyMapper openAPIRequestBodyMapper = customMediaType.map( value -> new OpenAPIRequestBodyMapper(components, - operationAdaptor, semanticModel, value)).orElse(new OpenAPIRequestBodyMapper(components, - operationAdaptor, semanticModel)); + operationAdaptor, semanticModel, value, moduleMemberVisitor)).orElse( + new OpenAPIRequestBodyMapper(components, + operationAdaptor, semanticModel, moduleMemberVisitor)); openAPIRequestBodyMapper.handlePayloadAnnotation(requiredParameterNode, schema, annotation, apidocs); errors.addAll(openAPIRequestBodyMapper.getDiagnostics()); } else if ((annotation.annotReference().toString()).trim().equals(Constants.HTTP_PAYLOAD) && @@ -233,12 +240,13 @@ private List handleDefaultableAnnotationParameters(DefaultableParamet for (AnnotationNode annotation: annotations) { if ((annotation.annotReference().toString()).trim().equals(Constants.HTTP_HEADER)) { // Handle headers. - OpenAPIHeaderMapper openAPIHeaderMapper = new OpenAPIHeaderMapper(components, semanticModel, apidocs); + OpenAPIHeaderMapper openAPIHeaderMapper = new OpenAPIHeaderMapper(components, semanticModel, apidocs, + moduleMemberVisitor); parameters = openAPIHeaderMapper.setHeaderParameter(defaultableParameterNode); } else if ((annotation.annotReference().toString()).trim().equals(Constants.HTTP_QUERY)) { // Handle query parameter. OpenAPIQueryParameterMapper openAPIQueryParameterMapper = new OpenAPIQueryParameterMapper(apidocs, - components, semanticModel); + components, semanticModel, moduleMemberVisitor); parameters.add(openAPIQueryParameterMapper.createQueryParameter(defaultableParameterNode)); } } diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIQueryParameterMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIQueryParameterMapper.java index 39275a4d9..b70abfec4 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIQueryParameterMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIQueryParameterMapper.java @@ -54,10 +54,10 @@ import static io.ballerina.compiler.syntax.tree.SyntaxKind.SIMPLE_NAME_REFERENCE; import static io.ballerina.compiler.syntax.tree.SyntaxKind.STRING_LITERAL; import static io.ballerina.openapi.converter.utils.ConverterCommonUtils.getAnnotationNodesFromServiceNode; +import static io.ballerina.openapi.converter.utils.ConverterCommonUtils.getOpenApiSchema; import static io.ballerina.openapi.converter.utils.ConverterCommonUtils.handleReference; import static io.ballerina.openapi.converter.utils.ConverterCommonUtils.unescapeIdentifier; - /** * This class processes mapping query parameters in between Ballerina and OAS. * @@ -67,14 +67,16 @@ public class OpenAPIQueryParameterMapper { private final Components components; private final SemanticModel semanticModel; private final Map apidocs; + private final ModuleMemberVisitor moduleMemberVisitor; private final SyntaxKind[] validExpressionKind = {STRING_LITERAL, NUMERIC_LITERAL, BOOLEAN_LITERAL, LIST_CONSTRUCTOR, NIL_LITERAL, MAPPING_CONSTRUCTOR}; public OpenAPIQueryParameterMapper(Map apidocs, Components components, - SemanticModel semanticModel) { + SemanticModel semanticModel, ModuleMemberVisitor moduleMemberVisitor) { this.apidocs = apidocs; this.components = components; this.semanticModel = semanticModel; + this.moduleMemberVisitor = moduleMemberVisitor; } /** @@ -87,7 +89,7 @@ public Parameter createQueryParameter(RequiredParameterNode queryParam) { if (queryParam.typeName() instanceof BuiltinSimpleNameReferenceNode && isQuery) { QueryParameter queryParameter = new QueryParameter(); queryParameter.setName(unescapeIdentifier(queryParamName)); - Schema openApiSchema = ConverterCommonUtils.getOpenApiSchema(queryParam.typeName().toString().trim()); + Schema openApiSchema = getOpenApiSchema(queryParam.typeName().toString().trim()); queryParameter.setSchema(openApiSchema); queryParameter.setRequired(true); if (!apidocs.isEmpty() && queryParam.paramName().isPresent() && apidocs.containsKey(queryParamName)) { @@ -115,7 +117,8 @@ public Parameter createQueryParameter(RequiredParameterNode queryParam) { QueryParameter queryParameter = new QueryParameter(); queryParameter.setName(unescapeIdentifier(queryParamName)); SimpleNameReferenceNode queryNode = (SimpleNameReferenceNode) queryParam.typeName(); - OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components, queryNode); + OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components, moduleMemberVisitor); + TypeSymbol typeSymbol = (TypeSymbol) semanticModel.symbol(queryNode).orElseThrow(); componentMapper.createComponentSchema(components.getSchemas(), typeSymbol); Schema schema = new Schema<>(); @@ -129,7 +132,7 @@ public Parameter createQueryParameter(RequiredParameterNode queryParam) { } else if (queryParam.typeName().kind() == SIMPLE_NAME_REFERENCE) { QueryParameter queryParameter = new QueryParameter(); Schema refSchema = handleReference(semanticModel, components, (SimpleNameReferenceNode) - queryParam.typeName()); + queryParam.typeName(), moduleMemberVisitor); queryParameter.setSchema(refSchema); queryParameter.setRequired(true); if (!apidocs.isEmpty() && apidocs.containsKey(queryParamName)) { @@ -157,7 +160,7 @@ public Parameter createQueryParameter(DefaultableParameterNode defaultableQueryP QueryParameter queryParameter = new QueryParameter(); if (defaultableQueryParam.typeName() instanceof BuiltinSimpleNameReferenceNode && isQuery) { queryParameter.setName(unescapeIdentifier(queryParamName)); - Schema openApiSchema = ConverterCommonUtils.getOpenApiSchema( + Schema openApiSchema = getOpenApiSchema( defaultableQueryParam.typeName().toString().trim()); queryParameter.setSchema(openApiSchema); if (!apidocs.isEmpty() && defaultableQueryParam.paramName().isPresent() && @@ -176,7 +179,7 @@ public Parameter createQueryParameter(DefaultableParameterNode defaultableQueryP } else if (defaultableQueryParam.typeName().kind() == SIMPLE_NAME_REFERENCE) { queryParameter.setName(unescapeIdentifier(queryParamName)); Schema refSchema = handleReference(semanticModel, components, - (SimpleNameReferenceNode) defaultableQueryParam.typeName()); + (SimpleNameReferenceNode) defaultableQueryParam.typeName(), moduleMemberVisitor); queryParameter.setSchema(refSchema); queryParameter.setRequired(true); if (!apidocs.isEmpty() && apidocs.containsKey(queryParamName)) { @@ -224,13 +227,13 @@ private QueryParameter handleArrayTypeQueryParameter(String queryParamName, Arra TypeDescriptorNode itemTypeNode = arrayNode.memberTypeDesc(); Schema itemSchema; if (arrayNode.memberTypeDesc().kind() == OPTIONAL_TYPE_DESC) { - itemSchema = ConverterCommonUtils.getOpenApiSchema( + itemSchema = getOpenApiSchema( ((OptionalTypeDescriptorNode) itemTypeNode).typeDescriptor().toString().trim()); itemSchema.setNullable(true); } else if (arrayNode.memberTypeDesc().kind() == SIMPLE_NAME_REFERENCE) { itemSchema = getItemSchemaForReference(arrayNode); } else { - itemSchema = ConverterCommonUtils.getOpenApiSchema(itemTypeNode.toString().trim()); + itemSchema = getOpenApiSchema(itemTypeNode.toString().trim()); } arraySchema.setItems(itemSchema); queryParameter.schema(arraySchema); @@ -243,7 +246,7 @@ private QueryParameter handleArrayTypeQueryParameter(String queryParamName, Arra private Schema getItemSchemaForReference(ArrayTypeDescriptorNode arrayNode) { SimpleNameReferenceNode record = (SimpleNameReferenceNode) arrayNode.memberTypeDesc(); - return handleReference(semanticModel, components, record); + return handleReference(semanticModel, components, record, moduleMemberVisitor); } /** @@ -267,7 +270,7 @@ private QueryParameter setOptionalQueryParameter(String queryParamName, Optional if (arrayNode.memberTypeDesc().kind() == SIMPLE_NAME_REFERENCE) { itemSchema = getItemSchemaForReference(arrayNode); } else { - itemSchema = ConverterCommonUtils.getOpenApiSchema(itemTypeNode.toString().trim()); + itemSchema = getOpenApiSchema(itemTypeNode.toString().trim()); } arraySchema.setItems(itemSchema); queryParameter.schema(arraySchema); @@ -286,7 +289,8 @@ private QueryParameter setOptionalQueryParameter(String queryParamName, Optional } return queryParameter; } else if (node.kind() == SIMPLE_NAME_REFERENCE) { - Schema refSchema = handleReference(semanticModel, components, (SimpleNameReferenceNode) node); + Schema refSchema = handleReference(semanticModel, components, (SimpleNameReferenceNode) node, + moduleMemberVisitor); queryParameter.setSchema(refSchema); if (isOptional.equals(Constants.FALSE)) { queryParameter.setRequired(true); @@ -296,7 +300,7 @@ private QueryParameter setOptionalQueryParameter(String queryParamName, Optional } return queryParameter; } else { - Schema openApiSchema = ConverterCommonUtils.getOpenApiSchema(node.toString().trim()); + Schema openApiSchema = getOpenApiSchema(node.toString().trim()); openApiSchema.setNullable(true); queryParameter.setSchema(openApiSchema); if (!apidocs.isEmpty() && apidocs.containsKey(queryParamName)) { diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIRequestBodyMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIRequestBodyMapper.java index 201e23879..2478629d7 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIRequestBodyMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIRequestBodyMapper.java @@ -26,7 +26,6 @@ import io.ballerina.compiler.syntax.tree.MappingConstructorExpressionNode; import io.ballerina.compiler.syntax.tree.MappingFieldNode; import io.ballerina.compiler.syntax.tree.Node; -import io.ballerina.compiler.syntax.tree.NonTerminalNode; import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode; import io.ballerina.compiler.syntax.tree.RequiredParameterNode; import io.ballerina.compiler.syntax.tree.SeparatedNodeList; @@ -73,7 +72,7 @@ public class OpenAPIRequestBodyMapper { private final SemanticModel semanticModel; private final String customMediaType; private final List diagnostics; - private NonTerminalNode rootNode; + private final ModuleMemberVisitor moduleMemberVisitor; /** * This constructor uses to create OpenAPIRequestBodyMapper instance when customMedia type enable. @@ -84,12 +83,14 @@ public class OpenAPIRequestBodyMapper { * @param customMediaType - custom media type */ public OpenAPIRequestBodyMapper(Components components, OperationAdaptor operationAdaptor, - SemanticModel semanticModel, String customMediaType) { + SemanticModel semanticModel, String customMediaType, + ModuleMemberVisitor moduleMemberVisitor) { this.components = components; this.operationAdaptor = operationAdaptor; this.semanticModel = semanticModel; this.customMediaType = customMediaType; this.diagnostics = new ArrayList<>(); + this.moduleMemberVisitor = moduleMemberVisitor; } /** @@ -100,8 +101,8 @@ public OpenAPIRequestBodyMapper(Components components, OperationAdaptor operatio * @param semanticModel - Semantic model for given ballerina service */ public OpenAPIRequestBodyMapper(Components components, OperationAdaptor operationAdaptor, - SemanticModel semanticModel) { - this(components, operationAdaptor, semanticModel, null); + SemanticModel semanticModel, ModuleMemberVisitor moduleMemberVisitor) { + this(components, operationAdaptor, semanticModel, null, moduleMemberVisitor); } public List getDiagnostics() { @@ -117,7 +118,6 @@ public List getDiagnostics() { */ public void handlePayloadAnnotation(RequiredParameterNode payloadNode, Map schema, AnnotationNode annotation, Map apiDocs) { - rootNode = payloadNode; if ((annotation.annotReference().toString()).trim().equals(Constants.HTTP_PAYLOAD)) { // Creating request body - required. RequestBody bodyParameter = new RequestBody(); @@ -230,7 +230,7 @@ private void handleArrayTypePayload(Map schema, ArrayTypeDescrip //handle record for components SimpleNameReferenceNode referenceNode = (SimpleNameReferenceNode) typeDescriptorNode; TypeSymbol typeSymbol = getReferenceTypeSymbol(semanticModel.symbol(referenceNode)); - OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components, referenceNode); + OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components, moduleMemberVisitor); componentMapper.createComponentSchema(schema, typeSymbol); diagnostics.addAll(componentMapper.getDiagnostics()); Schema itemSchema = new Schema(); @@ -285,7 +285,7 @@ private void createRequestBody(RequestBody bodyParameter, RequiredParameterNode private void handleReferencePayload(TypeSymbol typeSymbol, String recordName, Map schema, String mediaType, RequestBody bodyParameter) { //handle record for components - OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components, rootNode); + OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components, moduleMemberVisitor); componentMapper.createComponentSchema(schema, typeSymbol); diagnostics.addAll(componentMapper.getDiagnostics()); io.swagger.v3.oas.models.media.MediaType media = new io.swagger.v3.oas.models.media.MediaType(); diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIResourceMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIResourceMapper.java index ae33ba95a..8153cb89a 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIResourceMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIResourceMapper.java @@ -58,6 +58,7 @@ */ public class OpenAPIResourceMapper { private final SemanticModel semanticModel; + private final ModuleMemberVisitor moduleMemberVisitor; private final Paths pathObject = new Paths(); private final Components components = new Components(); private final List errors; @@ -69,9 +70,10 @@ public List getErrors() { /** * Initializes a resource parser for openApi. */ - OpenAPIResourceMapper(SemanticModel semanticModel) { + OpenAPIResourceMapper(SemanticModel semanticModel, ModuleMemberVisitor moduleMemberVisitor) { this.semanticModel = semanticModel; this.errors = new ArrayList<>(); + this.moduleMemberVisitor = moduleMemberVisitor; } public Components getComponents() { @@ -211,7 +213,7 @@ private Optional convertResourceToOperation(FunctionDefinition Map apiDocs = listAPIDocumentations(resource, op); //Add path parameters if in path and query parameters OpenAPIParameterMapper openAPIParameterMapper = new OpenAPIParameterMapper(resource, op, apiDocs, components, - semanticModel); + semanticModel, moduleMemberVisitor); openAPIParameterMapper.getResourceInputs(components, semanticModel); if (openAPIParameterMapper.getErrors().size() > 1 || (openAPIParameterMapper.getErrors().size() == 1 && !openAPIParameterMapper.getErrors().get(0).getCode().equals("OAS_CONVERTOR_113"))) { @@ -221,7 +223,7 @@ private Optional convertResourceToOperation(FunctionDefinition errors.addAll(openAPIParameterMapper.getErrors()); OpenAPIResponseMapper openAPIResponseMapper = new OpenAPIResponseMapper(semanticModel, components, - resource.location()); + resource.location(), moduleMemberVisitor); openAPIResponseMapper.getResourceOutput(resource, op); if (!openAPIResponseMapper.getErrors().isEmpty()) { errors.addAll(openAPIResponseMapper.getErrors()); diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIResponseMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIResponseMapper.java index 9fdc895e4..d8bf22c53 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIResponseMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIResponseMapper.java @@ -152,15 +152,18 @@ public class OpenAPIResponseMapper { private final List errors = new ArrayList<>(); private final Location location; private String httpMethod; + private ModuleMemberVisitor moduleMemberVisitor; public List getErrors() { return errors; } - public OpenAPIResponseMapper(SemanticModel semanticModel, Components components, Location location) { + public OpenAPIResponseMapper(SemanticModel semanticModel, Components components, Location location, + ModuleMemberVisitor moduleMemberVisitor) { this.semanticModel = semanticModel; this.components = components; this.location = location; + this.moduleMemberVisitor = moduleMemberVisitor; } /** @@ -585,7 +588,7 @@ private Optional handleQualifiedNameType(ApiResponses apiResponses if (typeSymbol.typeKind() == TypeDescKind.RECORD) { ApiResponses responses = handleRecordTypeSymbol(qNode.identifier().text().trim(), components.getSchemas(), customMediaPrefix, typeRef, - new OpenAPIComponentMapper(components, qNode), + new OpenAPIComponentMapper(components, moduleMemberVisitor), headers); apiResponses.putAll(responses); return Optional.of(apiResponses); @@ -882,7 +885,7 @@ private void handleReferenceResponse(OperationAdaptor operationAdaptor, SimpleNa Optional symbol = semanticModel.symbol(referenceNode); TypeSymbol typeSymbol = (TypeSymbol) symbol.orElseThrow(); //handle record for components - OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components, referenceNode); + OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components, moduleMemberVisitor); String mediaTypeString; // Check typeInclusion is related to the http status code if (referenceNode.parent().kind().equals(ARRAY_TYPE_DESC)) { diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIServiceMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIServiceMapper.java index a7fc0d9aa..a7d7cbf62 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIServiceMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIServiceMapper.java @@ -39,6 +39,7 @@ */ public class OpenAPIServiceMapper { private final SemanticModel semanticModel; + private final ModuleMemberVisitor moduleMemberVisitor; private final List errors = new ArrayList<>(); public List getErrors() { @@ -48,9 +49,10 @@ public List getErrors() { /** * Initializes a service parser for OpenApi. */ - public OpenAPIServiceMapper(SemanticModel semanticModel) { + public OpenAPIServiceMapper(SemanticModel semanticModel, ModuleMemberVisitor moduleMemberVisitor) { // Default object mapper is JSON mapper available in openApi utils. this.semanticModel = semanticModel; + this.moduleMemberVisitor = moduleMemberVisitor; } /** @@ -69,7 +71,7 @@ public OpenAPI convertServiceToOpenAPI(ServiceDeclarationNode service, OpenAPI o resource.add((FunctionDefinitionNode) function); } } - OpenAPIResourceMapper resourceMapper = new OpenAPIResourceMapper(this.semanticModel); + OpenAPIResourceMapper resourceMapper = new OpenAPIResourceMapper(this.semanticModel, this.moduleMemberVisitor); openapi.setPaths(resourceMapper.getPaths(resource)); openapi.setComponents(resourceMapper.getComponents()); errors.addAll(resourceMapper.getErrors()); diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/utils/ConverterCommonUtils.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/utils/ConverterCommonUtils.java index c9f9b032f..3e5a74836 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/utils/ConverterCommonUtils.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/utils/ConverterCommonUtils.java @@ -52,6 +52,7 @@ import io.ballerina.openapi.converter.diagnostic.ExceptionDiagnostic; import io.ballerina.openapi.converter.diagnostic.OpenAPIConverterDiagnostic; import io.ballerina.openapi.converter.model.OASResult; +import io.ballerina.openapi.converter.service.ModuleMemberVisitor; import io.ballerina.openapi.converter.service.OpenAPIComponentMapper; import io.ballerina.runtime.api.utils.IdentifierUtils; import io.ballerina.tools.diagnostics.Diagnostic; @@ -527,13 +528,13 @@ public static String unescapeIdentifier(String parameterName) { } public static Schema handleReference(SemanticModel semanticModel, Components components, - SimpleNameReferenceNode record) { + SimpleNameReferenceNode record, ModuleMemberVisitor moduleMemberVisitor) { Schema refSchema = new Schema<>(); // Creating request body - required. Optional symbol = semanticModel.symbol(record); if (symbol.isPresent() && symbol.get() instanceof TypeSymbol) { String recordName = record.name().toString().trim(); - OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components); + OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components, moduleMemberVisitor); componentMapper.createComponentSchema(components.getSchemas(), (TypeSymbol) symbol.get()); refSchema.set$ref(ConverterCommonUtils.unescapeIdentifier(recordName)); } diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/utils/ServiceToOpenAPIConverterUtils.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/utils/ServiceToOpenAPIConverterUtils.java index 47bbf5768..77c9a0bf9 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/utils/ServiceToOpenAPIConverterUtils.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/utils/ServiceToOpenAPIConverterUtils.java @@ -203,7 +203,8 @@ private static void extractServiceNodes(String serviceName, List availab */ public static OASResult generateOAS(OASGenerationMetaInfo oasGenerationMetaInfo) { ServiceDeclarationNode serviceDefinition = oasGenerationMetaInfo.getServiceDeclarationNode(); - LinkedHashSet listeners = collectListeners(oasGenerationMetaInfo.getProject());; + ModuleMemberVisitor moduleMemberVisitor = extractNodesFromProject(oasGenerationMetaInfo.getProject()); + LinkedHashSet listeners = moduleMemberVisitor.getListenerDeclarationNodes(); SemanticModel semanticModel = oasGenerationMetaInfo.getSemanticModel(); String openApiFileName = oasGenerationMetaInfo.getOpenApiFileName(); Path ballerinaFilePath = oasGenerationMetaInfo.getBallerinaFilePath(); @@ -214,7 +215,8 @@ public static OASResult generateOAS(OASGenerationMetaInfo oasGenerationMetaInfo) OpenAPI openapi = oasResult.getOpenAPI().get(); if (openapi.getPaths() == null) { // Take base path of service - OpenAPIServiceMapper openAPIServiceMapper = new OpenAPIServiceMapper(semanticModel); + OpenAPIServiceMapper openAPIServiceMapper = new OpenAPIServiceMapper(semanticModel, + moduleMemberVisitor); // 02. Filter and set the ServerURLs according to endpoints. Complete the server section in OAS openapi = OpenAPIEndpointMapper.ENDPOINT_MAPPER.getServers(openapi, listeners, serviceDefinition); // 03. Filter path and component sections in OAS. @@ -489,17 +491,15 @@ private static OASResult resolveContractPath(List di * * @param project - current project */ - public static LinkedHashSet collectListeners(Project project) { + public static ModuleMemberVisitor extractNodesFromProject(Project project) { ModuleMemberVisitor balNodeVisitor = new ModuleMemberVisitor(); - LinkedHashSet listeners = new LinkedHashSet<>(); project.currentPackage().moduleIds().forEach(moduleId -> { Module module = project.currentPackage().module(moduleId); module.documentIds().forEach(documentId -> { SyntaxTree syntaxTreeDoc = module.document(documentId).syntaxTree(); syntaxTreeDoc.rootNode().accept(balNodeVisitor); - listeners.addAll(balNodeVisitor.getListenerDeclarationNodes()); }); }); - return listeners; + return balNodeVisitor; } } diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ConstraintTests.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ConstraintTests.java index 874b0f3eb..6da34aee4 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ConstraintTests.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ConstraintTests.java @@ -1,19 +1,19 @@ /* - * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2022, WSO2 LLC. (https://www.wso2.com). * - * WSO2 Inc. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. - * You may obtain a copy of the License at + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ package io.ballerina.openapi.generators.openapi; diff --git a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ModuleReferenceTests.java b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ModuleReferenceTests.java index ede75ddcc..91b299a12 100644 --- a/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ModuleReferenceTests.java +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ModuleReferenceTests.java @@ -73,7 +73,7 @@ public void testRecordReferenceWithReadOnly() throws IOException { TestUtils.compareWithGeneratedFile(ballerinaFilePath, "readonly.yaml"); } - @Test (enabled = false) + @Test () public void testListenersInSeparateModule() throws IOException { Path ballerinaFilePath = RES_DIR.resolve("listeners_in_separate_module.bal"); String osName = System.getProperty("os.name"); @@ -82,7 +82,7 @@ public void testListenersInSeparateModule() throws IOException { TestUtils.compareWithGeneratedFile(ballerinaFilePath, yamlFile); } - @Test (enabled = false) + @Test () public void testListenersInSeparateFiles() throws IOException { Path ballerinaFilePath = RES_DIR.resolve("listeners_in_separate_file.bal"); String osName = System.getProperty("os.name"); diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/arrayTypeResponse.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/arrayTypeResponse.yaml index fbbb9956a..f0df4278f 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/arrayTypeResponse.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/arrayTypeResponse.yaml @@ -51,6 +51,7 @@ components: id: type: string name: + maxLength: 14 type: string description: type: string diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/array.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/array.yaml index b9b20e03b..25987bcad 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/array.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/array.yaml @@ -20,22 +20,44 @@ paths: $ref: '#/components/schemas/Person' responses: "500": - description: Found unexpected output + description: Internal server error content: - text/plain: + application/json: schema: - type: string + $ref: '#/components/schemas/ErrorPayload' components: schemas: PersonDetailsItemsString: minLength: 7 type: string + ErrorPayload: + type: object + properties: + reason: + type: string + description: Reason phrase + path: + type: string + description: Request path + method: + type: string + description: Method type of the request + message: + type: string + description: Error message + timestamp: + type: string + description: Timestamp of the error + status: + type: integer + description: Relevant HTTP status code + format: int32 PersonFeeItemsNumber: maximum: 445.4 type: number format: float PersonLimitItemsInteger: - maximum: 67 + maximum: 67.0 type: integer format: int32 Hobby: diff --git a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/record_field.yaml b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/record_field.yaml index 25ffb4b6e..64786f551 100644 --- a/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/record_field.yaml +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/record_field.yaml @@ -20,13 +20,35 @@ paths: $ref: '#/components/schemas/Person' responses: "500": - description: Found unexpected output + description: Internal server error content: - text/plain: + application/json: schema: - type: string + $ref: '#/components/schemas/ErrorPayload' components: schemas: + ErrorPayload: + type: object + properties: + reason: + type: string + description: Reason phrase + path: + type: string + description: Request path + method: + type: string + description: Method type of the request + message: + type: string + description: Error message + timestamp: + type: string + description: Timestamp of the error + status: + type: integer + description: Relevant HTTP status code + format: int32 Address: minLength: 5 type: string From c7cf4fb73c55b9316f5fd1bc145b0cb9b4aa4109 Mon Sep 17 00:00:00 2001 From: lnash94 Date: Tue, 22 Aug 2023 16:22:11 +0530 Subject: [PATCH 5/5] Resolve conflict and apply project API changes --- .../service/OpenAPIComponentMapper.java | 26 +++++++------------ .../converter/utils/ConverterCommonUtils.java | 7 ++--- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java index a4c9b6f26..e905813ca 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/OpenAPIComponentMapper.java @@ -132,10 +132,8 @@ public void createComponentSchema(Map schema, TypeSymbol typeSym new ConstraintAnnotation.ConstraintAnnotationBuilder(); ((TypeReferenceTypeSymbol) typeSymbol).definition().getName().ifPresent(name -> { for (TypeDefinitionNode typeDefinitionNode : typeDefinitionNodes) { - if (typeDefinitionNode.typeName().text().equals(name)) { - if (typeDefinitionNode.metadata().isPresent()) { + if (typeDefinitionNode.typeName().text().equals(name) && typeDefinitionNode.metadata().isPresent()) { extractedConstraintAnnotation(typeDefinitionNode.metadata().get(), constraintBuilder); - } } } }); @@ -365,22 +363,22 @@ private ObjectSchema generateObjectSchemaFromRecordFields(Map sc List required = new ArrayList<>(); componentSchema.setDescription(apiDocs.get(componentName)); Map schemaProperties = new LinkedHashMap<>(); - RecordTypeDescriptorNode record = null; - // Get the record type descriptor node from given record type symbol + RecordTypeDescriptorNode recordNode = null; + // Get the recordNode type descriptor node from given recordNode type symbol for (TypeDefinitionNode typeDefinitionNode : typeDefinitionNodes) { if (typeDefinitionNode.typeName().text().equals(componentName)) { if (typeDefinitionNode.typeDescriptor().kind().equals(SyntaxKind.RECORD_TYPE_DESC)) { - record = (RecordTypeDescriptorNode) typeDefinitionNode.typeDescriptor(); + recordNode = (RecordTypeDescriptorNode) typeDefinitionNode.typeDescriptor(); } else if (typeDefinitionNode.typeDescriptor().kind().equals(SyntaxKind.INTERSECTION_TYPE_DESC)) { IntersectionTypeDescriptorNode intersecNode = (IntersectionTypeDescriptorNode) typeDefinitionNode.typeDescriptor(); Node leftTypeDesc = intersecNode.leftTypeDesc(); Node rightTypeDesc = intersecNode.rightTypeDesc(); if (leftTypeDesc.kind().equals(SyntaxKind.RECORD_TYPE_DESC)) { - record = (RecordTypeDescriptorNode) leftTypeDesc; + recordNode = (RecordTypeDescriptorNode) leftTypeDesc; } if (rightTypeDesc.kind().equals(SyntaxKind.RECORD_TYPE_DESC)) { - record = (RecordTypeDescriptorNode) rightTypeDesc; + recordNode = (RecordTypeDescriptorNode) rightTypeDesc; } } } @@ -389,8 +387,8 @@ record = (RecordTypeDescriptorNode) rightTypeDesc; for (Map.Entry field : rfields.entrySet()) { ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder = new ConstraintAnnotation.ConstraintAnnotationBuilder(); - if (record != null) { - for (Node node : record.fields()) { + if (recordNode != null) { + for (Node node : recordNode.fields()) { if (node instanceof RecordFieldNode) { RecordFieldNode fieldNode = (RecordFieldNode) node; if (fieldNode.fieldName().toString().equals(field.getKey())) { @@ -437,7 +435,7 @@ record = (RecordTypeDescriptorNode) rightTypeDesc; property.setNullable(nullable); schema = components.getSchemas(); } - // Add API documentation for record field + // Add API documentation for recordNode field if (apiDocs.containsKey(fieldName)) { property.setDescription(apiDocs.get(fieldName)); } @@ -738,7 +736,6 @@ private Schema setConstraintValueToSchema(ConstraintAnnotation constraintAnnot, } try { - BigDecimal minimum = null; BigDecimal maximum = null; if (constraintAnnot.getMinValue().isPresent()) { @@ -777,14 +774,11 @@ private Schema setConstraintValueToSchema(ConstraintAnnotation constraintAnnot, */ private void extractedConstraintAnnotation(MetadataNode metadata, ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder) { - NodeList annotations = metadata.annotations(); - annotations.stream().filter(annot -> - (annot.annotReference() instanceof QualifiedNameReferenceNode && + annotations.stream().filter(annot -> (annot.annotReference() instanceof QualifiedNameReferenceNode && ((QualifiedNameReferenceNode) annot.annotReference()).modulePrefix().text() .equals("constraint"))) .forEach(value -> { - Optional fieldValues = value.annotValue(); if (fieldValues.isPresent()) { Spliterator spliterator = fieldValues.get().fields().spliterator(); diff --git a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/utils/ConverterCommonUtils.java b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/utils/ConverterCommonUtils.java index 3e5a74836..009e29c6d 100644 --- a/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/utils/ConverterCommonUtils.java +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/utils/ConverterCommonUtils.java @@ -528,12 +528,13 @@ public static String unescapeIdentifier(String parameterName) { } public static Schema handleReference(SemanticModel semanticModel, Components components, - SimpleNameReferenceNode record, ModuleMemberVisitor moduleMemberVisitor) { + SimpleNameReferenceNode recordNode, + ModuleMemberVisitor moduleMemberVisitor) { Schema refSchema = new Schema<>(); // Creating request body - required. - Optional symbol = semanticModel.symbol(record); + Optional symbol = semanticModel.symbol(recordNode); if (symbol.isPresent() && symbol.get() instanceof TypeSymbol) { - String recordName = record.name().toString().trim(); + String recordName = recordNode.name().toString().trim(); OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components, moduleMemberVisitor); componentMapper.createComponentSchema(components.getSchemas(), (TypeSymbol) symbol.get()); refSchema.set$ref(ConverterCommonUtils.unescapeIdentifier(recordName));