diff --git a/README.md b/README.md index 05e87cc..5dddf16 100644 --- a/README.md +++ b/README.md @@ -313,7 +313,7 @@ The element must be a number inside the specified `integer` and `fraction` range - Applies to : `String`, `Byte`, `Short`, `Int`, `Long`, `BigDecimal`, `BigInteger`, `Float`, `Lists` -- SDL : `directive @Digits(integer : Int!, fraction : Int!, message : String = "graphql.validation.Digits.message") on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION` +- SDL : `directive @Digits(integer : Int!, fraction : Int, message : String = "graphql.validation.Digits.message") on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION` - Message : `graphql.validation.Digits.message` diff --git a/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java b/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java index ddb7a7f..3cfd0e9 100644 --- a/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java +++ b/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java @@ -27,6 +27,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import static graphql.schema.GraphQLTypeUtil.isList; import static graphql.validation.rules.ValidationEnvironment.ValidatedElement.FIELD; @@ -198,23 +199,28 @@ protected boolean isOneOfTheseTypes(GraphQLInputType inputType, Collection assertExpectedArgType(argName, "Int")); + } - Number value = argument.getValue(); - if (value == null) { - return assertExpectedArgType(argName, "Int"); - } - return value.intValue(); + /** + * Returns an optional integer argument from a directive (or its default), or empty Optional if the argument is null. + * + * @param directive the directive to check + * @param argName the argument name + * @return an optional null value + */ + protected Optional getIntArgOpt(GraphQLAppliedDirective directive, String argName) { + return Optional.ofNullable(directive.getArgument(argName)) + .map(GraphQLAppliedDirectiveArgument::getValue) + .map(Number::intValue); } /** diff --git a/src/main/java/graphql/validation/constraints/standard/DigitsConstraint.java b/src/main/java/graphql/validation/constraints/standard/DigitsConstraint.java index fe94bbd..a8f963e 100644 --- a/src/main/java/graphql/validation/constraints/standard/DigitsConstraint.java +++ b/src/main/java/graphql/validation/constraints/standard/DigitsConstraint.java @@ -10,6 +10,7 @@ import java.math.BigDecimal; import java.util.Collections; import java.util.List; +import java.util.Optional; import static graphql.validation.constraints.GraphQLScalars.GRAPHQL_NUMBER_AND_STRING_TYPES; @@ -21,14 +22,14 @@ public DigitsConstraint() { @Override public Documentation getDocumentation() { return Documentation.newDocumentation() - .messageTemplate(getMessageTemplate()) - .description("The element must be a number inside the specified `integer` and `fraction` range.") - .example("buyCar( carCost : Float @Digits(integer : 5, fraction : 2) : DriverDetails") - .applicableTypes(GRAPHQL_NUMBER_AND_STRING_TYPES) - .directiveSDL("directive @Digits(integer : Int!, fraction : Int!, message : String = \"%s\") " + - "on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION", - getMessageTemplate()) - .build(); + .messageTemplate(getMessageTemplate()) + .description("The element must be a number inside the specified `integer` and optionally inside `fraction` range.") + .example("buyCar( carCost : Float @Digits(integer : 5, fraction : 2) : DriverDetails") + .applicableTypes(GRAPHQL_NUMBER_AND_STRING_TYPES) + .directiveSDL("directive @Digits(integer : Int!, fraction : Int, message : String = \"%s\") " + + "on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION", + getMessageTemplate()) + .build(); } @Override @@ -43,28 +44,40 @@ protected List runConstraint(ValidationEnvironment validationEnvir GraphQLAppliedDirective directive = validationEnvironment.getContextObject(GraphQLAppliedDirective.class); int maxIntegerLength = getIntArg(directive, "integer"); - int maxFractionLength = getIntArg(directive, "fraction"); + Optional maxFractionLengthOpt = getIntArgOpt(directive, "fraction"); boolean isOk; try { BigDecimal bigNum = asBigDecimal(validatedValue); - isOk = isOk(bigNum, maxIntegerLength, maxFractionLength); + boolean isFractionPartOk = maxFractionLengthOpt + .map(maxFractionLength -> isFractionPartOk(bigNum, maxFractionLength)) + .orElse(true); + + isOk = isFractionPartOk && isIntegerPartOk(bigNum, maxIntegerLength); } catch (NumberFormatException e) { isOk = false; } if (!isOk) { - return mkError(validationEnvironment, "integer", maxIntegerLength, "fraction", maxFractionLength); + return mkError( + validationEnvironment, + "integer", + maxIntegerLength, "fraction", + maxFractionLengthOpt.map(Object::toString).orElse("unlimited") + ); } return Collections.emptyList(); } - private boolean isOk(BigDecimal bigNum, int maxIntegerLength, int maxFractionLength) { - int integerPartLength = bigNum.precision() - bigNum.scale(); - int fractionPartLength = Math.max(bigNum.scale(), 0); + private static boolean isIntegerPartOk(BigDecimal bigNum, int maxIntegerLength) { + final int integerPartLength = bigNum.precision() - bigNum.scale(); + return maxIntegerLength >= integerPartLength; + } - return maxIntegerLength >= integerPartLength && maxFractionLength >= fractionPartLength; + private static boolean isFractionPartOk(BigDecimal bigNum, int maxFractionLength) { + final int fractionPartLength = Math.max(bigNum.scale(), 0); + return maxFractionLength >= fractionPartLength; } @Override diff --git a/src/test/groovy/graphql/validation/constraints/standard/DigitsConstraintTest.groovy b/src/test/groovy/graphql/validation/constraints/standard/DigitsConstraintTest.groovy index 772ec36..701746c 100644 --- a/src/test/groovy/graphql/validation/constraints/standard/DigitsConstraintTest.groovy +++ b/src/test/groovy/graphql/validation/constraints/standard/DigitsConstraintTest.groovy @@ -23,6 +23,7 @@ class DigitsConstraintTest extends BaseConstraintTestSupport { 'field( arg : String @Digits(integer : 5, fraction : 2) ) : ID' | null | '' 'field( arg : String @Digits(integer : 5, fraction : 2) ) : ID' | Byte.valueOf("0") | '' 'field( arg : String @Digits(integer : 5, fraction : 2) ) : ID' | Double.valueOf("500.2") | '' + 'field( arg : String @Digits(integer : 5) ) : ID' | Double.valueOf("500.2345678") | '' 'field( arg : String @Digits(integer : 5, fraction : 2) ) : ID' | new BigDecimal("-12345.12") | '' 'field( arg : String @Digits(integer : 5, fraction : 2) ) : ID' | new BigDecimal("-123456.12") | 'Digits;path=/arg;val:-123456.12;\t'