Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for providing constraint failure message #77

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions ballerina/constraint.bal
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,21 @@ public annotation StringConstraints String on type, record field;
# The annotation, which is used for the constraints of the `anydata[]` type.
public annotation ArrayConstraints Array on type, record field;

# Represents the common fields of the constraint annotation records.
#
# + message - The error message to be used in case of a constraint violation
public type CommonFields record {|
string message?;
|};

# Represents the constraints associated with `int` type.
#
# + minValue - The inclusive lower bound of the constrained type
# + maxValue - The inclusive upper bound of the constrained type
# + minValueExclusive - The exclusive lower bound of the constrained type
# + maxValueExclusive - The exclusive upper bound of the constrained type
public type IntConstraints record {|
*CommonFields;
int minValue?;
int maxValue?;
int minValueExclusive?;
Expand All @@ -51,6 +59,7 @@ public type IntConstraints record {|
# + minValueExclusive - The exclusive lower bound of the constrained type
# + maxValueExclusive - The exclusive upper bound of the constrained type
public type FloatConstraints record {|
*CommonFields;
float minValue?;
float maxValue?;
float minValueExclusive?;
Expand All @@ -64,6 +73,7 @@ public type FloatConstraints record {|
# + minValueExclusive - The exclusive lower bound of the constrained type
# + maxValueExclusive - The exclusive upper bound of the constrained type
public type NumberConstraints record {|
*CommonFields;
decimal minValue?;
decimal maxValue?;
decimal minValueExclusive?;
Expand All @@ -77,6 +87,7 @@ public type NumberConstraints record {|
# + maxLength - The inclusive upper bound of the number of characters of the constrained `string` type
# + pattern - The regular expression to be matched with the constrained `string` type
public type StringConstraints record {|
*CommonFields;
int length?;
int minLength?;
int maxLength?;
Expand All @@ -89,6 +100,7 @@ public type StringConstraints record {|
# + minLength - The inclusive lower bound of the number of members of the constrained type
# + maxLength - The inclusive upper bound of the number of members of the constrained type
public type ArrayConstraints record {|
*CommonFields;
int length?;
int minLength?;
int maxLength?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class Constants {
public static final String CONSTRAINT_MIN_LENGTH = "minLength";
public static final String CONSTRAINT_MAX_LENGTH = "maxLength";
public static final String CONSTRAINT_PATTERN = "pattern";
public static final String CONSTRAINT_MESSAGE = "message";

static final String GENERIC_ERROR = "Error";
static final String VALIDATION_ERROR = "ValidationError";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com) 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.
*/

package io.ballerina.stdlib.constraint;

import java.util.List;
import java.util.stream.Collectors;

import static io.ballerina.stdlib.constraint.Constants.SYMBOL_SEPARATOR;
import static io.ballerina.stdlib.constraint.ErrorUtils.buildDefaultErrorMessage;

/**
* Represents a constraint violation error information.
*/
public class ConstraintErrorInfo {
final String path;
final String message;
final List<String> failedConstraints;
final boolean onMemberType;

public ConstraintErrorInfo(String path, String message, List<String> failedConstraints) {
this.path = path;
this.message = message;
this.failedConstraints = failedConstraints;
this.onMemberType = false;
}

public ConstraintErrorInfo(String path, String message, List<String> failedConstraints, boolean onMemberType) {
this.path = path;
this.message = message;
this.failedConstraints = failedConstraints;
this.onMemberType = onMemberType;
}

public String getPath() {
return path;
}

public String getMessage() {
return message;
}

public boolean hasMessage() {
return !onMemberType && message != null;
}

public String getErrorMessage() {
return message != null ? message : buildDefaultErrorMessage(failedConstraints);
}

public List<String> getFailedConstraintsWithPath() {
return failedConstraints.stream().map(
failedConstraint -> path + SYMBOL_SEPARATOR + failedConstraint).collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,30 @@ public static Object validate(Object value, BTypedesc typedesc) {
if (value instanceof BError) {
return ErrorUtils.buildTypeConversionError((BError) value);
}
List<String> failedConstraints = validateAfterTypeConversionInternal(value, type, SYMBOL_DOLLAR_SIGN);
if (!failedConstraints.isEmpty()) {
return ErrorUtils.buildValidationError(failedConstraints);
List<ConstraintErrorInfo> failedConstraintsInfo = validateAfterTypeConversionInternal(value, type,
SYMBOL_DOLLAR_SIGN);
if (!failedConstraintsInfo.isEmpty()) {
return ErrorUtils.buildValidationError(failedConstraintsInfo);
}
return value;
} catch (InternalValidationException e) {
return ErrorUtils.createGenericError(e.getMessage());
} catch (RuntimeException e) {
return ErrorUtils.buildUnexpectedError(e);
}
}

public static Object validateNew(Object value, BTypedesc typedesc) {
try {
Type type = typedesc.getDescribingType();
value = cloneWithTargetType(value, type);
if (value instanceof BError) {
return ErrorUtils.buildTypeConversionError((BError) value);
}
List<ConstraintErrorInfo> failedConstraintsInfo = validateAfterTypeConversionInternal(value, type,
SYMBOL_DOLLAR_SIGN);
if (!failedConstraintsInfo.isEmpty()) {
return ErrorUtils.buildValidationError(failedConstraintsInfo);
}
return value;
} catch (InternalValidationException e) {
Expand All @@ -69,9 +90,10 @@ public static Object validate(Object value, BTypedesc typedesc) {

public static Object validateAfterTypeConversion(Object value, Type type) {
try {
List<String> failedConstraints = validateAfterTypeConversionInternal(value, type, SYMBOL_DOLLAR_SIGN);
if (!failedConstraints.isEmpty()) {
return ErrorUtils.buildValidationError(failedConstraints);
List<ConstraintErrorInfo> failedConstraintsInfo = validateAfterTypeConversionInternal(
value, type, SYMBOL_DOLLAR_SIGN);
if (!failedConstraintsInfo.isEmpty()) {
return ErrorUtils.buildValidationError(failedConstraintsInfo);
}
return value;
} catch (InternalValidationException e) {
Expand All @@ -81,35 +103,35 @@ public static Object validateAfterTypeConversion(Object value, Type type) {
}
}

private static List<String> validateArrayMembers(Object value, ArrayType type, String path) {
private static List<ConstraintErrorInfo> validateArrayMembers(Object value, ArrayType type, String path) {
Type memberType = type.getElementType();
BArray members = ((BArray) value);
List<String> failedConstraints = new ArrayList<>();
List<ConstraintErrorInfo> failedConstraintsInfo = new ArrayList<>();
for (int i = 0; i < members.getLength(); i++) {
failedConstraints.addAll(validateAfterTypeConversionInternal(members.get(i), memberType, path +
failedConstraintsInfo.addAll(validateAfterTypeConversionInternal(members.get(i), memberType, path +
SYMBOL_OPEN_SQUARE_BRACKET + i + SYMBOL_CLOSE_SQUARE_BRACKET));
}
return failedConstraints;
return failedConstraintsInfo;
}

private static List<String> validateAfterTypeConversionInternal(Object value, Type type, String path) {
private static List<ConstraintErrorInfo> validateAfterTypeConversionInternal(Object value, Type type, String path) {
if (type instanceof ArrayType) {
return validateArrayMembers(value, (ArrayType) type, path);
}
List<String> failedConstraints = new ArrayList<>();
List<ConstraintErrorInfo> failedConstraintsInfo = new ArrayList<>();
if (type.isReadOnly()) {
type = getTypeFromReadOnly(type);
}
if (type instanceof AnnotatableType) {
AbstractAnnotations annotations = getAnnotationImpl(type, failedConstraints);
annotations.validate(value, (AnnotatableType) type, path);
AbstractAnnotations annotations = getAnnotationImpl(type, failedConstraintsInfo);
annotations.validate(value, (AnnotatableType) type, path, false);
} else if (type instanceof UnionType) {
Optional<Type> matchingType = getMatchingType(value, type);
if (matchingType.isPresent()) {
return validateAfterTypeConversionInternal(value, matchingType.get(), path);
}
}
return failedConstraints;
return failedConstraintsInfo;
}

private static Type getTypeFromReadOnly(Type type) {
Expand Down Expand Up @@ -152,11 +174,12 @@ private static Optional<Type> getMatchingType(Object value, Type type) {
return Optional.empty();
}

private static AbstractAnnotations getAnnotationImpl(Type type, List<String> failedConstraints) {
private static AbstractAnnotations getAnnotationImpl(Type type,
List<ConstraintErrorInfo> failedConstraintsInfo) {
if (type instanceof RecordType) {
return new RecordFieldAnnotations(failedConstraints);
return new RecordFieldAnnotations(failedConstraintsInfo);
} else {
return new TypeAnnotations(failedConstraints);
return new TypeAnnotations(failedConstraintsInfo);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,23 @@
package io.ballerina.stdlib.constraint;

import io.ballerina.runtime.api.creators.ErrorCreator;
import io.ballerina.runtime.api.creators.ValueCreator;
import io.ballerina.runtime.api.utils.StringUtils;
import io.ballerina.runtime.api.values.BError;
import io.ballerina.runtime.api.values.BMap;
import io.ballerina.runtime.api.values.BString;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import static io.ballerina.stdlib.constraint.Constants.GENERIC_ERROR;
import static io.ballerina.stdlib.constraint.Constants.SYMBOL_COMMA;
import static io.ballerina.stdlib.constraint.Constants.SYMBOL_DOT;
import static io.ballerina.stdlib.constraint.Constants.SYMBOL_SINGLE_QUOTE;
import static io.ballerina.stdlib.constraint.Constants.TYPE_CONVERSION_ERROR;
import static io.ballerina.stdlib.constraint.Constants.VALIDATION_ERROR;

/**
* Utility functions related to errors.
Expand All @@ -40,7 +46,7 @@ public class ErrorUtils {
private static final String TYPE_CONVERSION_ERROR_MESSAGE = "Type conversion failed due to typedesc and value " +
"mismatch.";
private static final String VALIDATION_ERROR_MESSAGE_PREFIX = "Validation failed for ";
private static final String VALIDATION_ERROR_MESSAGE_SUFFIX = " constraint(s).";
private static final String VALIDATION_ERROR_MESSAGE_SUFFIX = " constraint(s)";

static BError buildUnexpectedError(RuntimeException e) {
if (e instanceof BError) {
Expand All @@ -49,15 +55,78 @@ static BError buildUnexpectedError(RuntimeException e) {
return createGenericError(UNEXPECTED_ERROR_MESSAGE);
}

static BError buildValidationError(List<String> failedConstraints) {
static BError buildValidationError(List<ConstraintErrorInfo> failedConstraintsInfo) {
List<String> customErrorMsgList = new ArrayList<>();
List<String> restErrorMsgList = new ArrayList<>();
List<String> causeMsgList = new ArrayList<>();
Map<String, List<String>> details = new TreeMap<>();
for (ConstraintErrorInfo constraintErrorInfo : failedConstraintsInfo) {
causeMsgList.addAll(constraintErrorInfo.getFailedConstraintsWithPath());
if (constraintErrorInfo.hasMessage()) {
customErrorMsgList.add(constraintErrorInfo.getMessage());
} else {
restErrorMsgList.addAll(constraintErrorInfo.getFailedConstraintsWithPath());
}
String path = constraintErrorInfo.getPath();
if (details.containsKey(path)) {
details.get(path).add(constraintErrorInfo.getErrorMessage());
} else {
details.put(path, new ArrayList<>(List.of(constraintErrorInfo.getErrorMessage())));
}
}
BMap<BString, Object> errorDetails = buildErrorDetails(details);
if (customErrorMsgList.isEmpty()) {
return createError(buildDefaultErrorMessage(causeMsgList), errorDetails);
}
BError cause = createGenericError(buildDefaultErrorMessage(causeMsgList));
if (!restErrorMsgList.isEmpty()) {
customErrorMsgList.add(buildDefaultErrorMessage(restErrorMsgList));
}
return createError(buildErrorMessage(customErrorMsgList), cause, errorDetails);
}

static String buildDefaultErrorMessage(List<String> failedConstraints) {
Collections.sort(failedConstraints);
StringBuilder errorMsg = new StringBuilder(VALIDATION_ERROR_MESSAGE_PREFIX);
for (String constraint : failedConstraints) {
errorMsg.append(SYMBOL_SINGLE_QUOTE).append(constraint).append(SYMBOL_SINGLE_QUOTE + SYMBOL_COMMA);
}
errorMsg.deleteCharAt(errorMsg.length() - 1);
errorMsg.append(VALIDATION_ERROR_MESSAGE_SUFFIX);
return createError(errorMsg.toString(), VALIDATION_ERROR);
errorMsg.append(VALIDATION_ERROR_MESSAGE_SUFFIX).append(SYMBOL_DOT);
return errorMsg.toString();
}

static BMap<BString, Object> buildErrorDetails(Map<String, List<String>> details) {
BMap<BString, Object> errorDetails = ValueCreator.createMapValue();
for (Map.Entry<String, List<String>> entry : details.entrySet()) {
String failedConstraintMessage = buildErrorMessage(entry.getValue());
errorDetails.put(StringUtils.fromString(entry.getKey()), StringUtils.fromString(failedConstraintMessage));
}
return errorDetails;
}

static String buildErrorMessage(List<String> list) {
int size = list.size();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < size; i++) {
builder.append(trimMessage(list.get(i)));

if (i < size - 2) {
builder.append(", ");
} else if (i == size - 2) {
builder.append(" and ");
}
}
builder.append(SYMBOL_DOT);
return builder.toString();
}

static String trimMessage(String message) {
message = message.trim();
if (message.endsWith(SYMBOL_DOT)) {
return message.substring(0, message.length() - 1);
}
return message;
}

static BError buildTypeConversionError(BError err) {
Expand All @@ -69,13 +138,18 @@ static BError createGenericError(String errMessage) {
StringUtils.fromString(errMessage), null, null);
}

static BError createError(String errMessage, String errorType) {
return ErrorCreator.createError(ModuleUtils.getModule(), errorType,
StringUtils.fromString(errMessage), null, null);
static BError createError(String errMessage, BMap<BString, Object> details) {
return ErrorCreator.createError(ModuleUtils.getModule(), Constants.VALIDATION_ERROR,
StringUtils.fromString(errMessage), null, details);
}

static BError createError(String errMessage, BError err, String errorType) {
return ErrorCreator.createError(ModuleUtils.getModule(), errorType,
StringUtils.fromString(errMessage), err, null);
}

static BError createError(String errMessage, BError err, BMap<BString, Object> details) {
return ErrorCreator.createError(ModuleUtils.getModule(), Constants.VALIDATION_ERROR,
StringUtils.fromString(errMessage), err, details);
}
}
Loading