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 aa5a3c2b4..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,8 +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", "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 new file mode 100644 index 000000000..44121cd2a --- /dev/null +++ b/openapi-bal-service/src/main/java/io/ballerina/openapi/converter/service/ConstraintAnnotation.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2022, WSO2 LLC. (https://www.wso2.com). + * + * 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 + * + * 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; + +import java.util.Optional; + +/** + * This @link ConstraintAnnotation} class represents the constraint annotations. + * + * @since 1.9.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 Optional getMinValue() { + return Optional.ofNullable(minValue); + } + + public Optional getMaxValue() { + return Optional.ofNullable(maxValue); + } + + public Optional getLength() { + return Optional.ofNullable(length); + } + + public Optional getMinLength() { + return Optional.ofNullable(minLength); + } + + public Optional getMaxLength() { + return Optional.ofNullable(maxLength); + } + + public Optional getMinValueExclusive() { + return Optional.ofNullable(minValueExclusive); + } + + public Optional getMaxValueExclusive() { + return Optional.ofNullable(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 withMaxValue(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/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 9eee7fa1d..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 @@ -36,7 +36,22 @@ 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.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.Node; +import io.ballerina.compiler.syntax.tree.NodeList; +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; 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; @@ -49,14 +64,19 @@ import io.swagger.v3.oas.models.media.Schema; 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; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; 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; @@ -69,14 +89,15 @@ * @since 2.0.0 */ public class OpenAPIComponentMapper { - private final Components components; private final List diagnostics; private final HashSet visitedTypeDefinitionNames = new HashSet<>(); + private final LinkedHashSet typeDefinitionNodes; - public OpenAPIComponentMapper(Components components) { - this.components = components; - this.diagnostics = new ArrayList<>(); + public OpenAPIComponentMapper(Components components, ModuleMemberVisitor moduleMemberVisitor) { + this.components = components; + this.diagnostics = new ArrayList<>(); + this.typeDefinitionNodes = moduleMemberVisitor.getTypeDefinitionNodes(); } public List getDiagnostics() { @@ -102,11 +123,22 @@ public void createComponentSchema(Map schema, TypeSymbol typeSym } TypeReferenceTypeSymbol typeRef = (TypeReferenceTypeSymbol) typeSymbol; TypeSymbol type = typeRef.typeDescriptor(); - if (type.typeKind() == TypeDescKind.INTERSECTION) { type = excludeReadonlyIfPresent(type); } + //Access module part node for finding the node to given typeSymbol. + ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder = + new ConstraintAnnotation.ConstraintAnnotationBuilder(); + ((TypeReferenceTypeSymbol) typeSymbol).definition().getName().ifPresent(name -> { + for (TypeDefinitionNode typeDefinitionNode : typeDefinitionNodes) { + if (typeDefinitionNode.typeName().text().equals(name) && typeDefinitionNode.metadata().isPresent()) { + extractedConstraintAnnotation(typeDefinitionNode.metadata().get(), constraintBuilder); + } + } + }); + ConstraintAnnotation constraintAnnot = constraintBuilder.build(); + switch (type.typeKind()) { case RECORD: // Handle typeInclusions with allOf type binding @@ -123,7 +155,8 @@ public void createComponentSchema(Map schema, TypeSymbol typeSym } break; case STRING: - schema.put(componentName, new StringSchema().description(typeDoc)); + schema.put(componentName, setConstraintValueToSchema(constraintAnnot, + new StringSchema().description(typeDoc))); components.setSchemas(schema); break; case JSON: @@ -132,21 +165,25 @@ public void createComponentSchema(Map schema, TypeSymbol typeSym 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: @@ -326,7 +363,43 @@ private ObjectSchema generateObjectSchemaFromRecordFields(Map sc List required = new ArrayList<>(); componentSchema.setDescription(apiDocs.get(componentName)); Map schemaProperties = new LinkedHashMap<>(); + 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)) { + 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)) { + recordNode = (RecordTypeDescriptorNode) leftTypeDesc; + } + if (rightTypeDesc.kind().equals(SyntaxKind.RECORD_TYPE_DESC)) { + recordNode = (RecordTypeDescriptorNode) rightTypeDesc; + } + } + } + } + for (Map.Entry field : rfields.entrySet()) { + ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder = + new ConstraintAnnotation.ConstraintAnnotationBuilder(); + if (recordNode != null) { + for (Node node : recordNode.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); @@ -362,10 +435,12 @@ private ObjectSchema generateObjectSchemaFromRecordFields(Map sc 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)); } + // Assign value to property - string, number, array + setConstraintValueToSchema(constraintAnnot, property); schemaProperties.put(fieldName, property); } componentSchema.setProperties(schemaProperties); @@ -642,4 +717,119 @@ 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-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 bad38bb07..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); + 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 d7578d3da..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); + 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 27f724bbe..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 @@ -72,6 +72,7 @@ public class OpenAPIRequestBodyMapper { private final SemanticModel semanticModel; private final String customMediaType; private final List diagnostics; + private final ModuleMemberVisitor moduleMemberVisitor; /** * This constructor uses to create OpenAPIRequestBodyMapper instance when customMedia type enable. @@ -82,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; } /** @@ -98,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() { @@ -115,7 +118,6 @@ public List getDiagnostics() { */ public void handlePayloadAnnotation(RequiredParameterNode payloadNode, Map schema, AnnotationNode annotation, Map apiDocs) { - 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, moduleMemberVisitor); 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, 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 b9c3f0242..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; } /** @@ -584,7 +587,8 @@ private Optional handleQualifiedNameType(ApiResponses apiResponses } 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, moduleMemberVisitor), headers); apiResponses.putAll(responses); return Optional.of(apiResponses); @@ -881,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); + 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..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 @@ -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,14 @@ public static String unescapeIdentifier(String parameterName) { } public static Schema handleReference(SemanticModel semanticModel, Components components, - SimpleNameReferenceNode record) { + 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(); - OpenAPIComponentMapper componentMapper = new OpenAPIComponentMapper(components); + 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)); } 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 new file mode 100644 index 000000000..6da34aee4 --- /dev/null +++ b/openapi-cli/src/test/java/io/ballerina/openapi/generators/openapi/ConstraintTests.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022, WSO2 LLC. (https://www.wso2.com). + * + * 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 + * + * 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 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/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/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/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/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 new file mode 100644 index 000000000..25987bcad --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/array.yaml @@ -0,0 +1,96 @@ +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: Internal server error + content: + application/json: + schema: + $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.0 + 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 new file mode 100644 index 000000000..64786f551 --- /dev/null +++ b/openapi-cli/src/test/resources/ballerina-to-openapi/expected_gen/constraint/record_field.yaml @@ -0,0 +1,82 @@ +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: Internal server error + content: + application/json: + schema: + $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 + 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 diff --git a/openapi-cli/src/test/resources/testng.xml b/openapi-cli/src/test/resources/testng.xml index 8c4eaeafb..da9a21e08 100644 --- a/openapi-cli/src/test/resources/testng.xml +++ b/openapi-cli/src/test/resources/testng.xml @@ -42,6 +42,7 @@ under the License. +