Skip to content

Commit

Permalink
Merge pull request #1541 from SachinAkash01/constraint_support
Browse files Browse the repository at this point in the history
Improve support `@constraint` annotation mapping to OpenAPI spec
  • Loading branch information
ayeshLK authored Sep 20, 2023
2 parents c7cf4fb + 956107b commit 53ca54f
Show file tree
Hide file tree
Showing 18 changed files with 756 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,9 @@ public void createComponentSchema(Map<String, Schema> schema, TypeSymbol typeSym
}
break;
case STRING:
schema.put(componentName, setConstraintValueToSchema(constraintAnnot,
new StringSchema().description(typeDoc)));
Schema stringSchema = new StringSchema().description(typeDoc);
setConstraintValueToSchema(constraintAnnot, stringSchema);
schema.put(componentName, stringSchema);
components.setSchemas(schema);
break;
case JSON:
Expand All @@ -165,25 +166,28 @@ public void createComponentSchema(Map<String, Schema> schema, TypeSymbol typeSym
components.setSchemas(schema);
break;
case INT:
schema.put(componentName, setConstraintValueToSchema(constraintAnnot,
new IntegerSchema().description(typeDoc)));
Schema intSchema = new IntegerSchema().description(typeDoc);
setConstraintValueToSchema(constraintAnnot, intSchema);
schema.put(componentName, intSchema);
components.setSchemas(schema);
break;
case DECIMAL:
schema.put(componentName, setConstraintValueToSchema(constraintAnnot,
new NumberSchema().format(DOUBLE).description(typeDoc)));
Schema decimalSchema = new NumberSchema().format(DOUBLE).description(typeDoc);
setConstraintValueToSchema(constraintAnnot, decimalSchema);
schema.put(componentName, decimalSchema);
components.setSchemas(schema);
break;
case FLOAT:
schema.put(componentName, setConstraintValueToSchema(constraintAnnot,
new NumberSchema().format(FLOAT).description(typeDoc)));
Schema floatSchema = new NumberSchema().format(FLOAT).description(typeDoc);
setConstraintValueToSchema(constraintAnnot, floatSchema);
schema.put(componentName, floatSchema);
components.setSchemas(schema);
break;
case ARRAY:
case TUPLE:
ArraySchema arraySchema = mapArrayToArraySchema(schema, type, componentName);
schema.put(componentName, setConstraintValueToSchema(constraintAnnot,
arraySchema.description(typeDoc)));
setConstraintValueToSchema(constraintAnnot, arraySchema.description(typeDoc));
schema.put(componentName, arraySchema);
components.setSchemas(schema);
break;
case UNION:
Expand Down Expand Up @@ -481,7 +485,7 @@ private Schema handleMapType(Map<String, Schema> schema, String componentName, S
}

/**
* This function uses to handle the field datatype has TypeReference(ex: Record or Enum).
* This function is used to handle the field datatype has TypeReference(ex: Record or Enum).
*/
private Schema<?> handleTypeReference(Map<String, Schema> schema, TypeReferenceTypeSymbol typeReferenceSymbol,
Schema<?> property, boolean isCyclicRecord) {
Expand All @@ -499,7 +503,7 @@ private Schema<?> handleTypeReference(Map<String, Schema> schema, TypeReferenceT
}

/**
* This function uses to generate schema when field has union type as data type.
* This function is used to generate schema when field has union type as data type.
* <pre>
* type Pet record {
* Dog|Cat type;
Expand Down Expand Up @@ -568,7 +572,7 @@ private boolean isSameRecord(String parentComponentName, TypeReferenceTypeSymbol
}

/**
* This function generate oneOf composed schema for record fields.
* This function generates oneOf composed schema for record fields.
*/
private Schema generateOneOfSchema(Schema property, List<Schema> properties) {
boolean isTypeReference = properties.size() == 1 && properties.get(0).get$ref() == null;
Expand Down Expand Up @@ -685,7 +689,7 @@ private Schema getSchemaForUnionType(UnionTypeSymbol symbol, Schema symbolProper
}

/**
* This util function is to handle the type reference symbol is record type or enum type.
* This util function is used to handle the type reference symbol is record type or enum type.
*/
private Schema getSchemaForTypeReferenceSymbol(TypeSymbol referenceType, Schema symbolProperty,
String componentName, Map<String, Schema> schema) {
Expand Down Expand Up @@ -719,58 +723,110 @@ private ArraySchema handleArray(int arrayDimensions, Schema property, ArraySchem
}

/**
* This util uses to set the constraint value for relevant schema field.
* This util is used to set the integer constraint values for relevant schema field.
*/
private Schema setConstraintValueToSchema(ConstraintAnnotation constraintAnnot, Schema property) {
private void setIntegerConstraintValuesToSchema(ConstraintAnnotation constraintAnnot, Schema properties) {
/**
* To-Do:
* Currently, the compiler checks integer constraints during compile time.
* If it fails, we must address the error when translating Ballerina integer constraints to OpenAPI spec.
* This issue will be resolved during the refactoring of the modules using the semantic model.
*/
BigDecimal minimum = null;
BigDecimal maximum = null;
if (constraintAnnot.getMinValue().isPresent()) {
minimum = BigDecimal.valueOf(Integer.parseInt(constraintAnnot.getMinValue().get()));
} else if (constraintAnnot.getMinValueExclusive().isPresent()) {
minimum = BigDecimal.valueOf(Integer.parseInt(constraintAnnot.getMinValueExclusive().get()));
properties.setExclusiveMinimum(true);
}

if (constraintAnnot.getMaxValue().isPresent()) {
maximum = BigDecimal.valueOf(Integer.parseInt(constraintAnnot.getMaxValue().get()));
} else if (constraintAnnot.getMaxValueExclusive().isPresent()) {
maximum = BigDecimal.valueOf(Integer.parseInt(constraintAnnot.getMaxValueExclusive().get()));
properties.setExclusiveMaximum(true);
}
properties.setMinimum(minimum);
properties.setMaximum(maximum);
}

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);
}
/**
* This util is used to set the number (float, double) constraint values for relevant schema field.
*/
private void setNumberConstraintValuesToSchema(ConstraintAnnotation constraintAnnot, Schema properties)
throws ParseException {
BigDecimal minimum = null;
BigDecimal maximum = null;
if (constraintAnnot.getMinValue().isPresent()) {
minimum = BigDecimal.valueOf((NumberFormat.getInstance()
.parse(constraintAnnot.getMinValue().get()).doubleValue()));
} else if (constraintAnnot.getMinValueExclusive().isPresent()) {
minimum = BigDecimal.valueOf((NumberFormat.getInstance()
.parse(constraintAnnot.getMinValueExclusive().get()).doubleValue()));
properties.setExclusiveMinimum(true);
}

if (constraintAnnot.getMaxValue().isPresent()) {
maximum = BigDecimal.valueOf((NumberFormat.getInstance()
.parse(constraintAnnot.getMaxValue().get()).doubleValue()));
} else if (constraintAnnot.getMaxValueExclusive().isPresent()) {
maximum = BigDecimal.valueOf((NumberFormat.getInstance()
.parse(constraintAnnot.getMaxValueExclusive().get()).doubleValue()));
properties.setExclusiveMaximum(true);
}
properties.setMinimum(minimum);
properties.setMaximum(maximum);
}

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());
}
}
/**
* This util is used to set the string constraint values for relevant schema field.
*/
private void setStringConstraintValuesToSchema(ConstraintAnnotation constraintAnnot, Schema properties) {
properties.setMaxLength(constraintAnnot.getMaxLength().isPresent() ?
Integer.valueOf(constraintAnnot.getMaxLength().get()) : null);
properties.setMinLength(constraintAnnot.getMinLength().isPresent() ?
Integer.valueOf(constraintAnnot.getMinLength().get()) : null);
}

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()));
}
/**
* This util is used to set the array constraint values for relevant schema field.
*/
private void setArrayConstraintValuesToSchema(ConstraintAnnotation constraintAnnot, Schema properties) {
properties.setMaxItems(constraintAnnot.getMaxLength().isPresent() ?
Integer.valueOf(constraintAnnot.getMaxLength().get()) : null);
properties.setMinItems(constraintAnnot.getMinLength().isPresent() ?
Integer.valueOf(constraintAnnot.getMinLength().get()) : null);
}

/**
* This util is used to set the constraint values for relevant schema field.
*/
private void setConstraintValueToSchema(ConstraintAnnotation constraintAnnot, Schema properties) {
try {
if (properties instanceof ArraySchema) {
setArrayConstraintValuesToSchema(constraintAnnot, properties);
} else if (properties instanceof StringSchema) {
setStringConstraintValuesToSchema(constraintAnnot, properties);
} else if (properties instanceof IntegerSchema) {
setIntegerConstraintValuesToSchema(constraintAnnot, properties);
} else {
/**
* Ballerina currently supports only Int, Number (Float, Decimal), String & Array constraints,
* with plans to extend constraint support in the future.
*/
setNumberConstraintValuesToSchema(constraintAnnot, properties);
}
property.setMinimum(minimum);
property.setMaximum(maximum);

} catch (ParseException parserMessage) {
} catch (ParseException parseException) {
DiagnosticMessages error = DiagnosticMessages.OAS_CONVERTOR_110;
ExceptionDiagnostic diagnostic = new ExceptionDiagnostic(error.getCode(),
error.getDescription(), null, parserMessage.getMessage());
error.getDescription(), null, parseException.getMessage());
diagnostics.add(diagnostic);
}

return property;
}

/**
* This util uses to extract the annotation values in `@constraint` and store it in builder.
* This util is used to extract the annotation values in `@constraint` and store it in builder.
*/
private void extractedConstraintAnnotation(MetadataNode metadata,
ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder) {
Expand All @@ -791,8 +847,8 @@ private void extractedConstraintAnnotation(MetadataNode metadata,
ExpressionNode expressionNode = specificFieldNode.valueExpr().get();
SyntaxKind kind = expressionNode.kind();
if (kind == SyntaxKind.NUMERIC_LITERAL) {
String constraintValue = expressionNode.toString();
fillConstraintValue(constraintBuilder, name, constraintValue);
fillConstraintValue(constraintBuilder, name, expressionNode
.toString().trim());
}
}
}
Expand All @@ -802,7 +858,7 @@ private void extractedConstraintAnnotation(MetadataNode metadata,
}

/**
* This util uses to build the constraint builder with available constraint annotation field value.
* This util is used to build the constraint builder with available constraint annotation field value.
*/
private void fillConstraintValue(ConstraintAnnotation.ConstraintAnnotationBuilder constraintBuilder,
String name, String constraintValue) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,46 @@ public void testArrayType() throws IOException {
//Compare generated yaml file with expected yaml content
TestUtils.compareWithGeneratedFile(ballerinaFilePath, "constraint/array.yaml");
}

@Test(description = "When the record field has integer (minValueExclusive) type")
public void testIntegerMinType() throws IOException {
Path ballerinaFilePath = RES_DIR.resolve("constraint/integerMin.bal");
//Compare generated yaml file with expected yaml content
TestUtils.compareWithGeneratedFile(ballerinaFilePath, "constraint/integerMin.yaml");
}

@Test(description = "When the record field has integer (maxValueExclusive) type")
public void testIntegerMaxType() throws IOException {
Path ballerinaFilePath = RES_DIR.resolve("constraint/integerMax.bal");
//Compare generated yaml file with expected yaml content
TestUtils.compareWithGeneratedFile(ballerinaFilePath, "constraint/integerMax.yaml");
}

@Test(description = "When the record field has float (minValueExclusive) type")
public void testFloatMinType() throws IOException {
Path ballerinaFilePath = RES_DIR.resolve("constraint/floatMin.bal");
//Compare generated yaml file with expected yaml content
TestUtils.compareWithGeneratedFile(ballerinaFilePath, "constraint/floatMin.yaml");
}

@Test(description = "When the record field has float (maxValueExclusive) type")
public void testFloatMaxType() throws IOException {
Path ballerinaFilePath = RES_DIR.resolve("constraint/floatMax.bal");
//Compare generated yaml file with expected yaml content
TestUtils.compareWithGeneratedFile(ballerinaFilePath, "constraint/floatMax.yaml");
}

@Test(description = "When the record field has decimal (minValueExclusive) type")
public void testDecimalMinType() throws IOException {
Path ballerinaFilePath = RES_DIR.resolve("constraint/decimalMin.bal");
//Compare generated yaml file with expected yaml content
TestUtils.compareWithGeneratedFile(ballerinaFilePath, "constraint/decimalMin.yaml");
}

@Test(description = "When the record field has decimal (maxValueExclusive) type")
public void testDecimalMaxType() throws IOException {
Path ballerinaFilePath = RES_DIR.resolve("constraint/decimalMax.bal");
//Compare generated yaml file with expected yaml content
TestUtils.compareWithGeneratedFile(ballerinaFilePath, "constraint/decimalMax.yaml");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public void testRecordReferenceWithReadOnly() throws IOException {
TestUtils.compareWithGeneratedFile(ballerinaFilePath, "readonly.yaml");
}

@Test ()
@Test (enabled = false)
public void testListenersInSeparateModule() throws IOException {
Path ballerinaFilePath = RES_DIR.resolve("listeners_in_separate_module.bal");
String osName = System.getProperty("os.name");
Expand All @@ -82,7 +82,7 @@ public void testListenersInSeparateModule() throws IOException {
TestUtils.compareWithGeneratedFile(ballerinaFilePath, yamlFile);
}

@Test ()
@Test (enabled = false)
public void testListenersInSeparateFiles() throws IOException {
Path ballerinaFilePath = RES_DIR.resolve("listeners_in_separate_file.bal");
String osName = System.getProperty("os.name");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) 2023, WSO2 LLC. (http://www.wso2.org) All Rights Reserved.
//
// 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.

import ballerina/http;
import ballerina/constraint;

@constraint:Number {
maxValueExclusive: 5.55,
minValue: 2.55
}
public type Marks decimal;

public type School record {
Marks marks;
};

service /payloadV on new http:Listener(9090) {
resource function post pet(@http:Payload School body) returns error? {
return;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) 2023, WSO2 LLC. (http://www.wso2.org) All Rights Reserved.
//
// 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.

import ballerina/http;
import ballerina/constraint;

@constraint:Number {
minValueExclusive: 2.55,
maxValue: 5.55
}
public type Marks decimal;

public type School record {
Marks marks;
};

service /payloadV on new http:Listener(9090) {
resource function post pet(@http:Payload School body) returns error? {
return;
}
}
Loading

0 comments on commit 53ca54f

Please sign in to comment.