From aeeef75bbf2f0d67285c682d8fac1589279822f3 Mon Sep 17 00:00:00 2001 From: AllirionX Date: Tue, 29 Oct 2019 19:05:20 +1300 Subject: [PATCH 1/7] Fix issue #6 were rules on nested input were not applied correctly --- .../rules/TargetedValidationRules.java | 73 +++++++++++-------- .../validation/rules/ValidationRules.java | 2 +- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/src/main/java/graphql/validation/rules/TargetedValidationRules.java b/src/main/java/graphql/validation/rules/TargetedValidationRules.java index 43e6a78..4809a9e 100644 --- a/src/main/java/graphql/validation/rules/TargetedValidationRules.java +++ b/src/main/java/graphql/validation/rules/TargetedValidationRules.java @@ -1,5 +1,18 @@ package graphql.validation.rules; +import static graphql.validation.rules.ValidationEnvironment.ValidatedElement.ARGUMENT; +import static graphql.validation.rules.ValidationEnvironment.ValidatedElement.FIELD; +import static graphql.validation.rules.ValidationEnvironment.ValidatedElement.INPUT_OBJECT_FIELD; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.springframework.web.servlet.tags.form.InputTag; + import graphql.Assert; import graphql.GraphQLError; import graphql.PublicApi; @@ -20,17 +33,6 @@ import graphql.validation.locale.LocaleUtil; import graphql.validation.util.Util; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import static graphql.validation.rules.ValidationEnvironment.ValidatedElement.ARGUMENT; -import static graphql.validation.rules.ValidationEnvironment.ValidatedElement.FIELD; -import static graphql.validation.rules.ValidationEnvironment.ValidatedElement.INPUT_OBJECT_FIELD; - /** * TargetedValidationRules is a holder of {@link graphql.validation.rules.ValidationRule}s targeted against a specific * type, field and possible argument via {@link ValidationCoordinates}. It then allows those rules @@ -40,14 +42,17 @@ @PublicApi public class TargetedValidationRules { + private final ValidationRules validationRules; + private final Map> rulesMap; public TargetedValidationRules(Builder builder) { this.rulesMap = new HashMap<>(builder.rulesMap); + this.validationRules=builder.validationRules; } - public static Builder newValidationRules() { - return new Builder(); + public static Builder newValidationRules(ValidationRules validationRules) { + return new Builder(validationRules); } public boolean isEmpty() { @@ -117,34 +122,37 @@ public List runValidationRules(DataFetchingEnvironment env, Messag .locale(defaultLocale) .build(); - for (ValidationRule rule : rules) { - List ruleErrors = runValidationImpl(rule, ruleEnvironment, inputType, argValue); - errors.addAll(ruleErrors); - } + errors.addAll(runValidationImpl(rules, ruleEnvironment, inputType, argValue)); } return errors; } @SuppressWarnings("unchecked") - private List runValidationImpl(ValidationRule rule, ValidationEnvironment validationEnvironment, GraphQLInputType inputType, Object validatedValue) { - List errors = rule.runValidation(validationEnvironment); + private List runValidationImpl(List rules, ValidationEnvironment validationEnvironment, GraphQLInputType inputType, Object validatedValue) { + List errors = new ArrayList(); + for(ValidationRule rule : rules) { + errors.addAll(rule.runValidation(validationEnvironment)); + } + if (validatedValue == null) { return errors; } inputType = (GraphQLInputType) GraphQLTypeUtil.unwrapNonNull(inputType); - if (GraphQLTypeUtil.isList(inputType)) { - List values = new ArrayList<>(FpKit.toCollection(validatedValue)); - List ruleErrors = walkListArg(rule, validationEnvironment, (GraphQLList) inputType, values); - errors.addAll(ruleErrors); + + if (GraphQLTypeUtil.isList(inputType)) { + List values = new ArrayList<>(FpKit.toCollection(validatedValue)); + List ruleErrors = walkListArg(rules, validationEnvironment, (GraphQLList) inputType, values); + errors.addAll(ruleErrors); } + if (inputType instanceof GraphQLInputObjectType) { if (validatedValue instanceof Map) { Map objectValue = (Map) validatedValue; - List ruleErrors = walkObjectArg(rule, validationEnvironment, (GraphQLInputObjectType) inputType, objectValue); + List ruleErrors = walkObjectArg(validationEnvironment, (GraphQLInputObjectType) inputType, objectValue); errors.addAll(ruleErrors); } else { Assert.assertShouldNeverHappen("How can there be a `input` object type '%s' that does not have a matching Map java value", GraphQLTypeUtil.simplePrint(inputType)); @@ -154,7 +162,7 @@ private List runValidationImpl(ValidationRule rule, ValidationEnvi } - private List walkObjectArg(ValidationRule rule, ValidationEnvironment validationEnvironment, GraphQLInputObjectType argumentType, Map objectMap) { + private List walkObjectArg(ValidationEnvironment validationEnvironment, GraphQLInputObjectType argumentType, Map objectMap) { List errors = new ArrayList<>(); // run them in a stable order @@ -162,7 +170,6 @@ private List walkObjectArg(ValidationRule rule, ValidationEnvironm for (GraphQLInputObjectField inputField : fieldDefinitions) { GraphQLInputType fieldType = inputField.getType(); - List directives = inputField.getDirectives(); Object validatedValue = objectMap.getOrDefault(inputField.getName(), inputField.getDefaultValue()); if (validatedValue == null) { continue; @@ -177,14 +184,15 @@ private List walkObjectArg(ValidationRule rule, ValidationEnvironm .directives(inputField.getDirectives()) .validatedElement(INPUT_OBJECT_FIELD) ); + + List rulesChild = validationRules.getRulesFor(newValidationEnvironment.getArgument(), newValidationEnvironment.getFieldDefinition(), newValidationEnvironment.getFieldsContainer()); + errors.addAll(runValidationImpl(rulesChild, newValidationEnvironment, fieldType, validatedValue)); - List ruleErrors = runValidationImpl(rule, newValidationEnvironment, fieldType, validatedValue); - errors.addAll(ruleErrors); } return errors; } - private List walkListArg(ValidationRule rule, ValidationEnvironment validationEnvironment, GraphQLList argumentType, List objectList) { + private List walkListArg(List rules, ValidationEnvironment validationEnvironment, GraphQLList argumentType, List objectList) { List errors = new ArrayList<>(); GraphQLInputType listItemType = Util.unwrapOneAndAllNonNull(argumentType); @@ -206,7 +214,7 @@ private List walkListArg(ValidationRule rule, ValidationEnvironmen .directives(directives) ); - List ruleErrors = runValidationImpl(rule, newValidationEnvironment, listItemType, value); + List ruleErrors = runValidationImpl(rules, newValidationEnvironment, listItemType, value); errors.addAll(ruleErrors); ix++; } @@ -214,8 +222,13 @@ private List walkListArg(ValidationRule rule, ValidationEnvironmen } public static class Builder { + ValidationRules validationRules; Map> rulesMap = new HashMap<>(); + public Builder(ValidationRules validationRules) { + this.validationRules=validationRules; + } + public Builder addRule(ValidationCoordinates coordinates, ValidationRule rule) { rulesMap.compute(coordinates, (key, listOfRules) -> { if (listOfRules == null) { diff --git a/src/main/java/graphql/validation/rules/ValidationRules.java b/src/main/java/graphql/validation/rules/ValidationRules.java index a7fdc43..43c43da 100644 --- a/src/main/java/graphql/validation/rules/ValidationRules.java +++ b/src/main/java/graphql/validation/rules/ValidationRules.java @@ -60,7 +60,7 @@ public OnValidationErrorStrategy getOnValidationErrorStrategy() { } public TargetedValidationRules buildRulesFor(GraphQLFieldDefinition fieldDefinition, GraphQLFieldsContainer fieldsContainer) { - TargetedValidationRules.Builder rulesBuilder = TargetedValidationRules.newValidationRules(); + TargetedValidationRules.Builder rulesBuilder = TargetedValidationRules.newValidationRules(this); ValidationCoordinates fieldCoordinates = ValidationCoordinates.newCoordinates(fieldsContainer, fieldDefinition); List fieldRules = getRulesFor(fieldDefinition, fieldsContainer); From 9b4200371675345d111585757b8ac9b5b9dbbc61 Mon Sep 17 00:00:00 2001 From: AllirionX Date: Tue, 29 Oct 2019 20:00:27 +1300 Subject: [PATCH 2/7] Fix import --- .../java/graphql/validation/rules/TargetedValidationRules.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/graphql/validation/rules/TargetedValidationRules.java b/src/main/java/graphql/validation/rules/TargetedValidationRules.java index 4809a9e..9e94e22 100644 --- a/src/main/java/graphql/validation/rules/TargetedValidationRules.java +++ b/src/main/java/graphql/validation/rules/TargetedValidationRules.java @@ -11,8 +11,6 @@ import java.util.Locale; import java.util.Map; -import org.springframework.web.servlet.tags.form.InputTag; - import graphql.Assert; import graphql.GraphQLError; import graphql.PublicApi; From 489473836e7be2bc5bbfbcc4e4c56e69a80a6b4d Mon Sep 17 00:00:00 2001 From: AllirionX Date: Tue, 29 Oct 2019 20:25:59 +1300 Subject: [PATCH 3/7] Fix compatibility issue with graphql-java-kickstart --- .../constraints/AbstractDirectiveConstraint.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java b/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java index ebc13e0..a707755 100644 --- a/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java +++ b/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java @@ -306,12 +306,13 @@ protected List mkError(ValidationEnvironment validationEnvironment * * @return true if one of the above */ - protected boolean isStringOrListOrMap(GraphQLInputType inputType) { + @Override + public boolean appliesToType(GraphQLInputType inputType) { GraphQLInputType unwrappedType = Util.unwrapOneAndAllNonNull(inputType); - return Scalars.GraphQLString.equals(unwrappedType) || - isList(inputType) || - (unwrappedType instanceof GraphQLInputObjectType); - } + return Scalars.GraphQLString.equals(unwrappedType) || isList(inputType) + || (unwrappedType instanceof GraphQLInputObjectType) + || (unwrappedType instanceof GraphQLTypeReference); + } /** * Casts the object as a Map with an assertion of it is not one From 2514579b99dd96b08e36496d619d4e4c1f25e86b Mon Sep 17 00:00:00 2001 From: AllirionX Date: Tue, 29 Oct 2019 20:39:05 +1300 Subject: [PATCH 4/7] Fix import --- .../validation/constraints/AbstractDirectiveConstraint.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java b/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java index a707755..9bea0ad 100644 --- a/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java +++ b/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java @@ -10,6 +10,7 @@ import graphql.schema.GraphQLFieldsContainer; import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLTypeReference; import graphql.schema.GraphQLScalarType; import graphql.schema.GraphQLTypeUtil; import graphql.validation.rules.ValidationEnvironment; From bff12cd57b19c73eb7305285d8ffb3af16d19314 Mon Sep 17 00:00:00 2001 From: AllirionX Date: Tue, 29 Oct 2019 21:08:08 +1300 Subject: [PATCH 5/7] Fix AbstractDirectiveConstraint --- .../constraints/AbstractDirectiveConstraint.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java b/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java index 9bea0ad..0f20757 100644 --- a/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java +++ b/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java @@ -307,13 +307,13 @@ protected List mkError(ValidationEnvironment validationEnvironment * * @return true if one of the above */ - @Override - public boolean appliesToType(GraphQLInputType inputType) { + protected boolean isStringOrListOrMap(GraphQLInputType inputType) { GraphQLInputType unwrappedType = Util.unwrapOneAndAllNonNull(inputType); - return Scalars.GraphQLString.equals(unwrappedType) || isList(inputType) - || (unwrappedType instanceof GraphQLInputObjectType) - || (unwrappedType instanceof GraphQLTypeReference); - } + return Scalars.GraphQLString.equals(unwrappedType) || + isList(inputType) || + (unwrappedType instanceof GraphQLInputObjectType) || + (unwrappedType instanceof GraphQLTypeReference); + } /** * Casts the object as a Map with an assertion of it is not one From d4a7596b77c579b530bab367ca7ae8d9fa708a37 Mon Sep 17 00:00:00 2001 From: AllirionX Date: Sat, 30 Nov 2019 00:29:04 +1300 Subject: [PATCH 6/7] Do not run constraint on exisint rule --- .../AbstractDirectiveConstraint.java | 812 +++++++++--------- 1 file changed, 412 insertions(+), 400 deletions(-) diff --git a/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java b/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java index 0f20757..e08b188 100644 --- a/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java +++ b/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java @@ -1,6 +1,18 @@ package graphql.validation.constraints; -import graphql.Assert; +import static graphql.schema.GraphQLTypeUtil.isList; +import static graphql.validation.rules.ValidationEnvironment.ValidatedElement.FIELD; +import static graphql.validation.util.Util.mkMap; +import static java.util.Collections.singletonList; + +import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + import graphql.GraphQLError; import graphql.PublicSpi; import graphql.Scalars; @@ -10,414 +22,414 @@ import graphql.schema.GraphQLFieldsContainer; import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLInputType; -import graphql.schema.GraphQLTypeReference; import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLTypeReference; import graphql.schema.GraphQLTypeUtil; import graphql.validation.rules.ValidationEnvironment; import graphql.validation.util.DirectivesAndTypeWalker; import graphql.validation.util.Util; -import java.lang.reflect.Array; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static graphql.schema.GraphQLTypeUtil.isList; -import static graphql.validation.rules.ValidationEnvironment.ValidatedElement.FIELD; -import static graphql.validation.util.Util.mkMap; -import static java.util.Collections.singletonList; - @SuppressWarnings("UnnecessaryLocalVariable") @PublicSpi public abstract class AbstractDirectiveConstraint implements DirectiveConstraint { - private final String name; - - public AbstractDirectiveConstraint(String name) { - this.name = name; - } - - - @Override - public String toString() { - return "@" + name; - } - - @Override - public String getName() { - return name; - } - - - protected String getMessageTemplate() { - return "graphql.validation." + getName() + ".message"; - } - - @Override - public boolean appliesTo(GraphQLFieldDefinition fieldDefinition, GraphQLFieldsContainer fieldsContainer) { - return false; - } - - @Override - public boolean appliesTo(GraphQLArgument argument, GraphQLFieldDefinition fieldDefinition, GraphQLFieldsContainer fieldsContainer) { - - boolean suitable = DirectivesAndTypeWalker.isSuitable(argument, (inputType, directive) -> { - boolean hasNamedDirective = directive.getName().equals(this.getName()); - if (hasNamedDirective) { - inputType = Util.unwrapNonNull(inputType); - boolean appliesToType = appliesToType(inputType); - if (appliesToType) { - return true; - } - // if they have a @Directive on there BUT it cant handle that type - // then is a really bad situation - String argType = GraphQLTypeUtil.simplePrint(inputType); - Assert.assertTrue(false, "The directive rule '%s' cannot be placed on elements of type '%s'", "@" + this.getName(), argType); - } - return false; - }); - return suitable; - } - - /** - * A derived class will be called to indicate whether this input type applies to the constraint - * - * @param inputType the input type - * - * @return true if the constraint can handle that type - */ - abstract protected boolean appliesToType(GraphQLInputType inputType); - - /** - * This is called to perform the constraint validation - * - * @param validationEnvironment the validation environment - * - * @return a list of errors or an empty one if there are no errors - */ - abstract protected List runConstraint(ValidationEnvironment validationEnvironment); - - - @SuppressWarnings("unchecked") - @Override - public List runValidation(ValidationEnvironment validationEnvironment) { - - // output fields are special - if (validationEnvironment.getValidatedElement() == FIELD) { - return runFieldValidationImpl(validationEnvironment); - } - - Object validatedValue = validationEnvironment.getValidatedValue(); - - // - // all the directives validation code does NOT care for NULL ness since the graphql engine covers that. - // eg a @NonNull validation directive makes no sense in graphql like it might in Java - // - GraphQLInputType inputType = Util.unwrapNonNull(validationEnvironment.getValidatedType()); - validationEnvironment = validationEnvironment.transform(b -> b.validatedType(inputType)); - - return runValidationImpl(validationEnvironment, inputType, validatedValue); - } - - private List runFieldValidationImpl(ValidationEnvironment validationEnvironment) { - return runConstraintOnDirectives(validationEnvironment); - } - - @SuppressWarnings("unchecked") - private List runValidationImpl(ValidationEnvironment validationEnvironment, GraphQLInputType inputType, Object validatedValue) { - return runConstraintOnDirectives(validationEnvironment); - } - - private List runConstraintOnDirectives(ValidationEnvironment validationEnvironment) { - - List errors = new ArrayList<>(); - List directives = validationEnvironment.getDirectives(); - directives = Util.sort(directives, GraphQLDirective::getName); - - for (GraphQLDirective directive : directives) { - // we get called for arguments and input field and field types which can have multiple directive constraints on them and hence no just for this one - boolean isOurDirective = directive.getName().equals(this.getName()); - if (!isOurDirective) { - continue; - } - - validationEnvironment = validationEnvironment.transform(b -> b.context(GraphQLDirective.class, directive)); - // - // now run the directive rule with this directive instance - List ruleErrors = this.runConstraint(validationEnvironment); - errors.addAll(ruleErrors); - } - return errors; - } - - - /** - * Returns true of the input type is one of the specified scalar types, regardless of non null ness - * - * @param inputType the type to check - * @param scalarTypes the array of scalar types - * - * @return true ifits oneof them - */ - protected boolean isOneOfTheseTypes(GraphQLInputType inputType, GraphQLScalarType... scalarTypes) { - GraphQLInputType unwrappedType = Util.unwrapNonNull(inputType); - for (GraphQLScalarType scalarType : scalarTypes) { - if (unwrappedType.getName().equals(scalarType.getName())) { - return true; - } - } - return false; - } - - /** - * Returns an integer argument from a directive (or its default) and throws an assertion of the argument is null - * - * @param directive the directive to check - * @param argName the argument name - * - * @return a non null value - */ - protected int getIntArg(GraphQLDirective directive, String argName) { - GraphQLArgument argument = directive.getArgument(argName); - if (argument == null) { - return assertExpectedArgType(argName, "Int"); - } - Number value = (Number) argument.getValue(); - if (value == null) { - value = (Number) argument.getDefaultValue(); - if (value == null) { - return assertExpectedArgType(argName, "Int"); - } - } - return value.intValue(); - } - - /** - * Returns an String argument from a directive (or its default) and throws an assertion of the argument is null - * - * @param directive the directive to check - * @param argName the argument name - * - * @return a non null value - */ - protected String getStrArg(GraphQLDirective directive, String argName) { - GraphQLArgument argument = directive.getArgument(argName); - if (argument == null) { - return assertExpectedArgType(argName, "String"); - } - String value = (String) argument.getValue(); - if (value == null) { - value = (String) argument.getDefaultValue(); - if (value == null) { - return assertExpectedArgType(argName, "String"); - } - } - return value; - } - - /** - * Returns an boolean argument from a directive (or its default) and throws an assertion of the argument is null - * - * @param directive the directive to check - * @param argName the argument name - * - * @return a non null value - */ - protected boolean getBoolArg(GraphQLDirective directive, String argName) { - GraphQLArgument argument = directive.getArgument(argName); - if (argument == null) { - return assertExpectedArgType(argName, "Boolean"); - } - Object value = argument.getValue(); - if (value == null) { - value = argument.getDefaultValue(); - if (value == null) { - return assertExpectedArgType(argName, "Boolean"); - } - } - return Boolean.parseBoolean(String.valueOf(value)); - } - - /** - * Returns the "message : String" argument from a directive or makes up one - * called "graphql.validation.{name}.message" - * - * @param directive the directive to check - * - * @return a non null value - */ - protected String getMessageTemplate(GraphQLDirective directive) { - String msg = null; - GraphQLArgument arg = directive.getArgument("message"); - if (arg != null) { - msg = (String) arg.getValue(); - if (msg == null) { - msg = (String) arg.getDefaultValue(); - } - } - if (msg == null) { - msg = "graphql.validation." + getName() + ".message"; - } - return msg; - } - - /** - * Creates a map of named parameters for message interpolation - * - * @param validatedValue the value being validated - * @param validationEnvironment the validation environment - * @param args must be an key / value array with String keys as the even params and values as then odd params - * - * @return a map of message parameters - */ - protected Map mkMessageParams(Object validatedValue, ValidationEnvironment validationEnvironment, Object... args) { - Map params = new LinkedHashMap<>(); - params.put("validatedValue", validatedValue); - params.put("constraint", getName()); - params.put("path", validationEnvironment.getValidatedPath()); - - params.putAll(mkMap(args)); - return params; - } - - - - /** - * Creates a new {@link graphql.GraphQLError} - * - * @param validationEnvironment the current validation environment - * @param directive the directive being run - * @param msgParams the map of parameters - * - * @return a list of a single error - */ - protected List mkError(ValidationEnvironment validationEnvironment, GraphQLDirective directive, Map msgParams) { - String messageTemplate = getMessageTemplate(directive); - GraphQLError error = validationEnvironment.getInterpolator().interpolate(messageTemplate, msgParams, validationEnvironment); - return singletonList(error); - } - - /** - * Return true if the type is a String or List type or {@link graphql.schema.GraphQLInputObjectType}, regardless of non null ness - * - * @param inputType the type to check - * - * @return true if one of the above - */ - protected boolean isStringOrListOrMap(GraphQLInputType inputType) { - GraphQLInputType unwrappedType = Util.unwrapOneAndAllNonNull(inputType); - return Scalars.GraphQLString.equals(unwrappedType) || - isList(inputType) || - (unwrappedType instanceof GraphQLInputObjectType) || - (unwrappedType instanceof GraphQLTypeReference); - } - - /** - * Casts the object as a Map with an assertion of it is not one - * - * @param value the object to turn into a map - * - * @return a Map - */ - @SuppressWarnings("ConstantConditions") - protected Map asMap(Object value) { - Assert.assertTrue(value instanceof Map, "The argument value MUST be a Map value"); - return (Map) value; - } - - /** - * Makes the object a BigDecimal with an assertion if we have no conversion of it - * - * @param value the object to turn into a BigDecimal - * - * @return a BigDecimal - */ - protected BigDecimal asBigDecimal(Object value) throws NumberFormatException { - if (value == null) { - return Assert.assertShouldNeverHappen("Validation cant handle null objects BigDecimals"); - } - if (value instanceof BigDecimal) { - return (BigDecimal) value; - } - String bdStr = ""; - if (value instanceof Number) { - bdStr = value.toString(); - } else if (value instanceof String) { - bdStr = value.toString(); - } else { - Assert.assertShouldNeverHappen("Validation cant handle objects of type '%s' as BigDecimals", value.getClass().getSimpleName()); - } - return new BigDecimal(bdStr); - } - - /** - * Makes the object a boolean with an assertion if we have no conversion of it - * - * @param value the boolean object - * - * @return a boolean - */ - protected boolean asBoolean(Object value) { - if (value == null) { - return Assert.assertShouldNeverHappen("Validation cant handle null objects Booleans"); - } - if (value instanceof Boolean) { - return (Boolean) value; - } else { - return Assert.assertShouldNeverHappen("Validation cant handle objects of type '%s' as Booleans", value.getClass().getSimpleName()); - } - } - - /** - * Returns the length of a String of the size of a list or size of a Map - * - * @param inputType the input type - * @param value the value - * - * @return the length of a String or Map or List - */ - protected int getStringOrObjectOrMapLength(GraphQLInputType inputType, Object value) { - int valLen; - if (value == null) { - valLen = 0; - } else if (Scalars.GraphQLString.equals(Util.unwrapNonNull(inputType))) { - valLen = String.valueOf(value).length(); - } else if (isList(inputType)) { - valLen = getListLength(value); - } else { - valLen = getObjectLen(value); - } - return valLen; - } - - private int getObjectLen(Object value) { - if (value == null) { - return 0; - } - Map map = asMap(value); - return map.size(); - } - - private int getListLength(Object value) { - if (value instanceof Collection) { - return ((Collection) value).size(); - } else if (value instanceof Iterable) { - int len = 0; - for (Object ignored : ((Iterable) value)) { - len++; - } - return len; - } else if (value != null && value.getClass().isArray()) { - return Array.getLength(value); - } - return 0; - } - - private T assertExpectedArgType(String argName, String typeName) { - return Assert.assertShouldNeverHappen("A validation directive MUST have a '%s' argument of type '%s' with a default value", argName, typeName); - } + private final String name; + + public AbstractDirectiveConstraint(String name) { + this.name = name; + } + + @Override + public String toString() { + return "@" + name; + } + + @Override + public String getName() { + return name; + } + + protected String getMessageTemplate() { + return "graphql.validation." + getName() + ".message"; + } + + @Override + public boolean appliesTo(GraphQLFieldDefinition fieldDefinition, GraphQLFieldsContainer fieldsContainer) { + return false; + } + + @Override + public boolean appliesTo(GraphQLArgument argument, GraphQLFieldDefinition fieldDefinition, + GraphQLFieldsContainer fieldsContainer) { + + boolean suitable = DirectivesAndTypeWalker.isSuitable(argument, (inputType, directive) -> { + boolean hasNamedDirective = directive.getName().equals(this.getName()); + if (hasNamedDirective) { + inputType = Util.unwrapNonNull(inputType); + boolean appliesToType = appliesToType(inputType); + if (appliesToType) { + return true; + } + // if they have a @Directive on there BUT it cant handle that type + // then is a really bad situation + String argType = GraphQLTypeUtil.simplePrint(inputType); + Assert.assertTrue(false, "The directive rule '%s' cannot be placed on elements of type '%s'", + "@" + this.getName(), argType); + } + return false; + }); + return suitable; + } + + /** + * A derived class will be called to indicate whether this input type applies to + * the constraint + * + * @param inputType the input type + * + * @return true if the constraint can handle that type + */ + abstract protected boolean appliesToType(GraphQLInputType inputType); + + /** + * This is called to perform the constraint validation + * + * @param validationEnvironment the validation environment + * + * @return a list of errors or an empty one if there are no errors + */ + abstract protected List runConstraint(ValidationEnvironment validationEnvironment); + + @SuppressWarnings("unchecked") + @Override + public List runValidation(ValidationEnvironment validationEnvironment) { + + // output fields are special + if (validationEnvironment.getValidatedElement() == FIELD) { + return runFieldValidationImpl(validationEnvironment); + } + + Object validatedValue = validationEnvironment.getValidatedValue(); + + // + // all the directives validation code does NOT care for NULL ness since the + // graphql engine covers that. + // eg a @NonNull validation directive makes no sense in graphql like it might in + // Java + // + GraphQLInputType inputType = Util.unwrapNonNull(validationEnvironment.getValidatedType()); + validationEnvironment = validationEnvironment.transform(b -> b.validatedType(inputType)); + + return runValidationImpl(validationEnvironment, inputType, validatedValue); + } + + private List runFieldValidationImpl(ValidationEnvironment validationEnvironment) { + return runConstraintOnDirectives(validationEnvironment); + } + + @SuppressWarnings("unchecked") + private List runValidationImpl(ValidationEnvironment validationEnvironment, + GraphQLInputType inputType, Object validatedValue) { + return runConstraint(validationEnvironment); + } + + private List runConstraintOnDirectives(ValidationEnvironment validationEnvironment) { + + List errors = new ArrayList<>(); + List directives = validationEnvironment.getDirectives(); + directives = Util.sort(directives, GraphQLDirective::getName); + + for (GraphQLDirective directive : directives) { + // we get called for arguments and input field and field types which can have + // multiple directive constraints on them and hence no just for this one + boolean isOurDirective = directive.getName().equals(this.getName()); + if (!isOurDirective) { + continue; + } + + validationEnvironment = validationEnvironment.transform(b -> b.context(GraphQLDirective.class, directive)); + // + // now run the directive rule with this directive instance + List ruleErrors = this.runConstraint(validationEnvironment); + errors.addAll(ruleErrors); + } + return errors; + } + + /** + * Returns true of the input type is one of the specified scalar types, + * regardless of non null ness + * + * @param inputType the type to check + * @param scalarTypes the array of scalar types + * + * @return true ifits oneof them + */ + protected boolean isOneOfTheseTypes(GraphQLInputType inputType, GraphQLScalarType... scalarTypes) { + GraphQLInputType unwrappedType = Util.unwrapNonNull(inputType); + for (GraphQLScalarType scalarType : scalarTypes) { + if (unwrappedType.getName().equals(scalarType.getName())) { + return true; + } + } + return false; + } + + /** + * Returns an integer argument from a directive (or its default) and throws an + * assertion of the argument is null + * + * @param directive the directive to check + * @param argName the argument name + * + * @return a non null value + */ + protected int getIntArg(GraphQLDirective directive, String argName) { + GraphQLArgument argument = directive.getArgument(argName); + if (argument == null) { + return assertExpectedArgType(argName, "Int"); + } + Number value = (Number) argument.getValue(); + if (value == null) { + value = (Number) argument.getDefaultValue(); + if (value == null) { + return assertExpectedArgType(argName, "Int"); + } + } + return value.intValue(); + } + + /** + * Returns an String argument from a directive (or its default) and throws an + * assertion of the argument is null + * + * @param directive the directive to check + * @param argName the argument name + * + * @return a non null value + */ + protected String getStrArg(GraphQLDirective directive, String argName) { + GraphQLArgument argument = directive.getArgument(argName); + if (argument == null) { + return assertExpectedArgType(argName, "String"); + } + String value = (String) argument.getValue(); + if (value == null) { + value = (String) argument.getDefaultValue(); + if (value == null) { + return assertExpectedArgType(argName, "String"); + } + } + return value; + } + + /** + * Returns an boolean argument from a directive (or its default) and throws an + * assertion of the argument is null + * + * @param directive the directive to check + * @param argName the argument name + * + * @return a non null value + */ + protected boolean getBoolArg(GraphQLDirective directive, String argName) { + GraphQLArgument argument = directive.getArgument(argName); + if (argument == null) { + return assertExpectedArgType(argName, "Boolean"); + } + Object value = argument.getValue(); + if (value == null) { + value = argument.getDefaultValue(); + if (value == null) { + return assertExpectedArgType(argName, "Boolean"); + } + } + return Boolean.parseBoolean(String.valueOf(value)); + } + + /** + * Returns the "message : String" argument from a directive or makes up one + * called "graphql.validation.{name}.message" + * + * @param directive the directive to check + * + * @return a non null value + */ + protected String getMessageTemplate(GraphQLDirective directive) { + String msg = null; + GraphQLArgument arg = directive.getArgument("message"); + if (arg != null) { + msg = (String) arg.getValue(); + if (msg == null) { + msg = (String) arg.getDefaultValue(); + } + } + if (msg == null) { + msg = "graphql.validation." + getName() + ".message"; + } + return msg; + } + + /** + * Creates a map of named parameters for message interpolation + * + * @param validatedValue the value being validated + * @param validationEnvironment the validation environment + * @param args must be an key / value array with String keys as + * the even params and values as then odd params + * + * @return a map of message parameters + */ + protected Map mkMessageParams(Object validatedValue, ValidationEnvironment validationEnvironment, + Object... args) { + Map params = new LinkedHashMap<>(); + params.put("validatedValue", validatedValue); + params.put("constraint", getName()); + params.put("path", validationEnvironment.getValidatedPath()); + + params.putAll(mkMap(args)); + return params; + } + + /** + * Creates a new {@link graphql.GraphQLError} + * + * @param validationEnvironment the current validation environment + * @param directive the directive being run + * @param msgParams the map of parameters + * + * @return a list of a single error + */ + protected List mkError(ValidationEnvironment validationEnvironment, GraphQLDirective directive, + Map msgParams) { + String messageTemplate = getMessageTemplate(directive); + GraphQLError error = validationEnvironment.getInterpolator().interpolate(messageTemplate, msgParams, + validationEnvironment); + return singletonList(error); + } + + /** + * Return true if the type is a String or List type or + * {@link graphql.schema.GraphQLInputObjectType}, regardless of non null ness + * + * @param inputType the type to check + * + * @return true if one of the above + */ + protected boolean isStringOrListOrMap(GraphQLInputType inputType) { + GraphQLInputType unwrappedType = Util.unwrapOneAndAllNonNull(inputType); + return Scalars.GraphQLString.equals(unwrappedType) || isList(inputType) + || (unwrappedType instanceof GraphQLInputObjectType) || (unwrappedType instanceof GraphQLTypeReference); + } + + /** + * Casts the object as a Map with an assertion of it is not one + * + * @param value the object to turn into a map + * + * @return a Map + */ + @SuppressWarnings("ConstantConditions") + protected Map asMap(Object value) { + Assert.assertTrue(value instanceof Map, "The argument value MUST be a Map value"); + return (Map) value; + } + + /** + * Makes the object a BigDecimal with an assertion if we have no conversion of + * it + * + * @param value the object to turn into a BigDecimal + * + * @return a BigDecimal + */ + protected BigDecimal asBigDecimal(Object value) throws NumberFormatException { + if (value == null) { + return Assert.assertShouldNeverHappen("Validation cant handle null objects BigDecimals"); + } + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + String bdStr = ""; + if (value instanceof Number) { + bdStr = value.toString(); + } else if (value instanceof String) { + bdStr = value.toString(); + } else { + Assert.assertShouldNeverHappen("Validation cant handle objects of type '%s' as BigDecimals", + value.getClass().getSimpleName()); + } + return new BigDecimal(bdStr); + } + + /** + * Makes the object a boolean with an assertion if we have no conversion of it + * + * @param value the boolean object + * + * @return a boolean + */ + protected boolean asBoolean(Object value) { + if (value == null) { + return Assert.assertShouldNeverHappen("Validation cant handle null objects Booleans"); + } + if (value instanceof Boolean) { + return (Boolean) value; + } else { + return Assert.assertShouldNeverHappen("Validation cant handle objects of type '%s' as Booleans", + value.getClass().getSimpleName()); + } + } + + /** + * Returns the length of a String of the size of a list or size of a Map + * + * @param inputType the input type + * @param value the value + * + * @return the length of a String or Map or List + */ + protected int getStringOrObjectOrMapLength(GraphQLInputType inputType, Object value) { + int valLen; + if (value == null) { + valLen = 0; + } else if (Scalars.GraphQLString.equals(Util.unwrapNonNull(inputType))) { + valLen = String.valueOf(value).length(); + } else if (isList(inputType)) { + valLen = getListLength(value); + } else { + valLen = getObjectLen(value); + } + return valLen; + } + + private int getObjectLen(Object value) { + if (value == null) { + return 0; + } + Map map = asMap(value); + return map.size(); + } + + private int getListLength(Object value) { + if (value instanceof Collection) { + return ((Collection) value).size(); + } else if (value instanceof Iterable) { + int len = 0; + for (Object ignored : ((Iterable) value)) { + len++; + } + return len; + } else if (value != null && value.getClass().isArray()) { + return Array.getLength(value); + } + return 0; + } + + private T assertExpectedArgType(String argName, String typeName) { + return Assert.assertShouldNeverHappen( + "A validation directive MUST have a '%s' argument of type '%s' with a default value", argName, + typeName); + } } From 6ea3a72993fbf145a2757b253c685a8439e6c0c1 Mon Sep 17 00:00:00 2001 From: AllirionX Date: Sun, 1 Dec 2019 17:16:58 +1300 Subject: [PATCH 7/7] Retrieve directives from schema directly instead of from inputField --- .../AbstractDirectiveConstraint.java | 804 +++++++++--------- .../rules/TargetedValidationRules.java | 381 +++++---- .../rules/ValidationEnvironment.java | 427 +++++----- 3 files changed, 819 insertions(+), 793 deletions(-) diff --git a/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java b/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java index e08b188..d42bdae 100644 --- a/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java +++ b/src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java @@ -4,7 +4,6 @@ import static graphql.validation.rules.ValidationEnvironment.ValidatedElement.FIELD; import static graphql.validation.util.Util.mkMap; import static java.util.Collections.singletonList; - import java.lang.reflect.Array; import java.math.BigDecimal; import java.util.ArrayList; @@ -12,7 +11,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; - +import graphql.Assert; import graphql.GraphQLError; import graphql.PublicSpi; import graphql.Scalars; @@ -33,403 +32,408 @@ @PublicSpi public abstract class AbstractDirectiveConstraint implements DirectiveConstraint { - private final String name; - - public AbstractDirectiveConstraint(String name) { - this.name = name; - } - - @Override - public String toString() { - return "@" + name; - } - - @Override - public String getName() { - return name; - } - - protected String getMessageTemplate() { - return "graphql.validation." + getName() + ".message"; - } - - @Override - public boolean appliesTo(GraphQLFieldDefinition fieldDefinition, GraphQLFieldsContainer fieldsContainer) { - return false; - } - - @Override - public boolean appliesTo(GraphQLArgument argument, GraphQLFieldDefinition fieldDefinition, - GraphQLFieldsContainer fieldsContainer) { - - boolean suitable = DirectivesAndTypeWalker.isSuitable(argument, (inputType, directive) -> { - boolean hasNamedDirective = directive.getName().equals(this.getName()); - if (hasNamedDirective) { - inputType = Util.unwrapNonNull(inputType); - boolean appliesToType = appliesToType(inputType); - if (appliesToType) { - return true; - } - // if they have a @Directive on there BUT it cant handle that type - // then is a really bad situation - String argType = GraphQLTypeUtil.simplePrint(inputType); - Assert.assertTrue(false, "The directive rule '%s' cannot be placed on elements of type '%s'", - "@" + this.getName(), argType); - } - return false; - }); - return suitable; - } - - /** - * A derived class will be called to indicate whether this input type applies to - * the constraint - * - * @param inputType the input type - * - * @return true if the constraint can handle that type - */ - abstract protected boolean appliesToType(GraphQLInputType inputType); - - /** - * This is called to perform the constraint validation - * - * @param validationEnvironment the validation environment - * - * @return a list of errors or an empty one if there are no errors - */ - abstract protected List runConstraint(ValidationEnvironment validationEnvironment); - - @SuppressWarnings("unchecked") - @Override - public List runValidation(ValidationEnvironment validationEnvironment) { - - // output fields are special - if (validationEnvironment.getValidatedElement() == FIELD) { - return runFieldValidationImpl(validationEnvironment); - } - - Object validatedValue = validationEnvironment.getValidatedValue(); - - // - // all the directives validation code does NOT care for NULL ness since the - // graphql engine covers that. - // eg a @NonNull validation directive makes no sense in graphql like it might in - // Java - // - GraphQLInputType inputType = Util.unwrapNonNull(validationEnvironment.getValidatedType()); - validationEnvironment = validationEnvironment.transform(b -> b.validatedType(inputType)); - - return runValidationImpl(validationEnvironment, inputType, validatedValue); - } - - private List runFieldValidationImpl(ValidationEnvironment validationEnvironment) { - return runConstraintOnDirectives(validationEnvironment); - } - - @SuppressWarnings("unchecked") - private List runValidationImpl(ValidationEnvironment validationEnvironment, - GraphQLInputType inputType, Object validatedValue) { - return runConstraint(validationEnvironment); - } - - private List runConstraintOnDirectives(ValidationEnvironment validationEnvironment) { - - List errors = new ArrayList<>(); - List directives = validationEnvironment.getDirectives(); - directives = Util.sort(directives, GraphQLDirective::getName); - - for (GraphQLDirective directive : directives) { - // we get called for arguments and input field and field types which can have - // multiple directive constraints on them and hence no just for this one - boolean isOurDirective = directive.getName().equals(this.getName()); - if (!isOurDirective) { - continue; - } - - validationEnvironment = validationEnvironment.transform(b -> b.context(GraphQLDirective.class, directive)); - // - // now run the directive rule with this directive instance - List ruleErrors = this.runConstraint(validationEnvironment); - errors.addAll(ruleErrors); - } - return errors; - } - - /** - * Returns true of the input type is one of the specified scalar types, - * regardless of non null ness - * - * @param inputType the type to check - * @param scalarTypes the array of scalar types - * - * @return true ifits oneof them - */ - protected boolean isOneOfTheseTypes(GraphQLInputType inputType, GraphQLScalarType... scalarTypes) { - GraphQLInputType unwrappedType = Util.unwrapNonNull(inputType); - for (GraphQLScalarType scalarType : scalarTypes) { - if (unwrappedType.getName().equals(scalarType.getName())) { - return true; - } - } - return false; - } - - /** - * Returns an integer argument from a directive (or its default) and throws an - * assertion of the argument is null - * - * @param directive the directive to check - * @param argName the argument name - * - * @return a non null value - */ - protected int getIntArg(GraphQLDirective directive, String argName) { - GraphQLArgument argument = directive.getArgument(argName); - if (argument == null) { - return assertExpectedArgType(argName, "Int"); - } - Number value = (Number) argument.getValue(); - if (value == null) { - value = (Number) argument.getDefaultValue(); - if (value == null) { - return assertExpectedArgType(argName, "Int"); - } - } - return value.intValue(); - } - - /** - * Returns an String argument from a directive (or its default) and throws an - * assertion of the argument is null - * - * @param directive the directive to check - * @param argName the argument name - * - * @return a non null value - */ - protected String getStrArg(GraphQLDirective directive, String argName) { - GraphQLArgument argument = directive.getArgument(argName); - if (argument == null) { - return assertExpectedArgType(argName, "String"); - } - String value = (String) argument.getValue(); - if (value == null) { - value = (String) argument.getDefaultValue(); - if (value == null) { - return assertExpectedArgType(argName, "String"); - } - } - return value; - } - - /** - * Returns an boolean argument from a directive (or its default) and throws an - * assertion of the argument is null - * - * @param directive the directive to check - * @param argName the argument name - * - * @return a non null value - */ - protected boolean getBoolArg(GraphQLDirective directive, String argName) { - GraphQLArgument argument = directive.getArgument(argName); - if (argument == null) { - return assertExpectedArgType(argName, "Boolean"); - } - Object value = argument.getValue(); - if (value == null) { - value = argument.getDefaultValue(); - if (value == null) { - return assertExpectedArgType(argName, "Boolean"); - } - } - return Boolean.parseBoolean(String.valueOf(value)); - } - - /** - * Returns the "message : String" argument from a directive or makes up one - * called "graphql.validation.{name}.message" - * - * @param directive the directive to check - * - * @return a non null value - */ - protected String getMessageTemplate(GraphQLDirective directive) { - String msg = null; - GraphQLArgument arg = directive.getArgument("message"); - if (arg != null) { - msg = (String) arg.getValue(); - if (msg == null) { - msg = (String) arg.getDefaultValue(); - } - } - if (msg == null) { - msg = "graphql.validation." + getName() + ".message"; - } - return msg; - } - - /** - * Creates a map of named parameters for message interpolation - * - * @param validatedValue the value being validated - * @param validationEnvironment the validation environment - * @param args must be an key / value array with String keys as - * the even params and values as then odd params - * - * @return a map of message parameters - */ - protected Map mkMessageParams(Object validatedValue, ValidationEnvironment validationEnvironment, - Object... args) { - Map params = new LinkedHashMap<>(); - params.put("validatedValue", validatedValue); - params.put("constraint", getName()); - params.put("path", validationEnvironment.getValidatedPath()); - - params.putAll(mkMap(args)); - return params; - } - - /** - * Creates a new {@link graphql.GraphQLError} - * - * @param validationEnvironment the current validation environment - * @param directive the directive being run - * @param msgParams the map of parameters - * - * @return a list of a single error - */ - protected List mkError(ValidationEnvironment validationEnvironment, GraphQLDirective directive, - Map msgParams) { - String messageTemplate = getMessageTemplate(directive); - GraphQLError error = validationEnvironment.getInterpolator().interpolate(messageTemplate, msgParams, - validationEnvironment); - return singletonList(error); - } - - /** - * Return true if the type is a String or List type or - * {@link graphql.schema.GraphQLInputObjectType}, regardless of non null ness - * - * @param inputType the type to check - * - * @return true if one of the above - */ - protected boolean isStringOrListOrMap(GraphQLInputType inputType) { - GraphQLInputType unwrappedType = Util.unwrapOneAndAllNonNull(inputType); - return Scalars.GraphQLString.equals(unwrappedType) || isList(inputType) - || (unwrappedType instanceof GraphQLInputObjectType) || (unwrappedType instanceof GraphQLTypeReference); - } - - /** - * Casts the object as a Map with an assertion of it is not one - * - * @param value the object to turn into a map - * - * @return a Map - */ - @SuppressWarnings("ConstantConditions") - protected Map asMap(Object value) { - Assert.assertTrue(value instanceof Map, "The argument value MUST be a Map value"); - return (Map) value; - } - - /** - * Makes the object a BigDecimal with an assertion if we have no conversion of - * it - * - * @param value the object to turn into a BigDecimal - * - * @return a BigDecimal - */ - protected BigDecimal asBigDecimal(Object value) throws NumberFormatException { - if (value == null) { - return Assert.assertShouldNeverHappen("Validation cant handle null objects BigDecimals"); - } - if (value instanceof BigDecimal) { - return (BigDecimal) value; - } - String bdStr = ""; - if (value instanceof Number) { - bdStr = value.toString(); - } else if (value instanceof String) { - bdStr = value.toString(); - } else { - Assert.assertShouldNeverHappen("Validation cant handle objects of type '%s' as BigDecimals", - value.getClass().getSimpleName()); - } - return new BigDecimal(bdStr); - } - - /** - * Makes the object a boolean with an assertion if we have no conversion of it - * - * @param value the boolean object - * - * @return a boolean - */ - protected boolean asBoolean(Object value) { - if (value == null) { - return Assert.assertShouldNeverHappen("Validation cant handle null objects Booleans"); - } - if (value instanceof Boolean) { - return (Boolean) value; - } else { - return Assert.assertShouldNeverHappen("Validation cant handle objects of type '%s' as Booleans", - value.getClass().getSimpleName()); - } - } - - /** - * Returns the length of a String of the size of a list or size of a Map - * - * @param inputType the input type - * @param value the value - * - * @return the length of a String or Map or List - */ - protected int getStringOrObjectOrMapLength(GraphQLInputType inputType, Object value) { - int valLen; - if (value == null) { - valLen = 0; - } else if (Scalars.GraphQLString.equals(Util.unwrapNonNull(inputType))) { - valLen = String.valueOf(value).length(); - } else if (isList(inputType)) { - valLen = getListLength(value); - } else { - valLen = getObjectLen(value); - } - return valLen; - } - - private int getObjectLen(Object value) { - if (value == null) { - return 0; - } - Map map = asMap(value); - return map.size(); - } - - private int getListLength(Object value) { - if (value instanceof Collection) { - return ((Collection) value).size(); - } else if (value instanceof Iterable) { - int len = 0; - for (Object ignored : ((Iterable) value)) { - len++; - } - return len; - } else if (value != null && value.getClass().isArray()) { - return Array.getLength(value); - } - return 0; - } - - private T assertExpectedArgType(String argName, String typeName) { - return Assert.assertShouldNeverHappen( - "A validation directive MUST have a '%s' argument of type '%s' with a default value", argName, - typeName); - } + private final String name; + + public AbstractDirectiveConstraint(String name) { + this.name = name; + } + + @Override + public String toString() { + return "@" + name; + } + + @Override + public String getName() { + return name; + } + + protected String getMessageTemplate() { + return "graphql.validation." + getName() + ".message"; + } + + @Override + public boolean appliesTo(GraphQLFieldDefinition fieldDefinition, + GraphQLFieldsContainer fieldsContainer) { + return false; + } + + @Override + public boolean appliesTo(GraphQLArgument argument, GraphQLFieldDefinition fieldDefinition, + GraphQLFieldsContainer fieldsContainer) { + + boolean suitable = DirectivesAndTypeWalker.isSuitable(argument, (inputType, directive) -> { + boolean hasNamedDirective = directive.getName().equals(this.getName()); + if (hasNamedDirective) { + inputType = Util.unwrapNonNull(inputType); + boolean appliesToType = appliesToType(inputType); + if (appliesToType) { + return true; + } + // if they have a @Directive on there BUT it cant handle that type + // then is a really bad situation + String argType = GraphQLTypeUtil.simplePrint(inputType); + Assert.assertTrue(false, + "The directive rule '%s' cannot be placed on elements of type '%s'", + "@" + this.getName(), argType); + } + return false; + }); + return suitable; + } + + /** + * A derived class will be called to indicate whether this input type applies to the constraint + * + * @param inputType the input type + * + * @return true if the constraint can handle that type + */ + abstract protected boolean appliesToType(GraphQLInputType inputType); + + /** + * This is called to perform the constraint validation + * + * @param validationEnvironment the validation environment + * + * @return a list of errors or an empty one if there are no errors + */ + abstract protected List runConstraint(ValidationEnvironment validationEnvironment); + + @SuppressWarnings("unchecked") + @Override + public List runValidation(ValidationEnvironment validationEnvironment) { + + // output fields are special + if (validationEnvironment.getValidatedElement() == FIELD) { + return runFieldValidationImpl(validationEnvironment); + } + + Object validatedValue = validationEnvironment.getValidatedValue(); + + // + // all the directives validation code does NOT care for NULL ness since the + // graphql engine covers that. + // eg a @NonNull validation directive makes no sense in graphql like it might in + // Java + // + GraphQLInputType inputType = Util.unwrapNonNull(validationEnvironment.getValidatedType()); + validationEnvironment = validationEnvironment.transform(b -> b.validatedType(inputType)); + + return runValidationImpl(validationEnvironment, inputType, validatedValue); + } + + private List runFieldValidationImpl(ValidationEnvironment validationEnvironment) { + return runConstraintOnDirectives(validationEnvironment); + } + + @SuppressWarnings("unchecked") + private List runValidationImpl(ValidationEnvironment validationEnvironment, + GraphQLInputType inputType, Object validatedValue) { + return runConstraintOnDirectives(validationEnvironment); + } + + private List runConstraintOnDirectives( + ValidationEnvironment validationEnvironment) { + + List errors = new ArrayList<>(); + List directives = validationEnvironment.getDirectives(); + directives = Util.sort(directives, GraphQLDirective::getName); + + for (GraphQLDirective directive : directives) { + // we get called for arguments and input field and field types which can have + // multiple directive constraints on them and hence no just for this one + boolean isOurDirective = directive.getName().equals(this.getName()); + if (!isOurDirective) { + continue; + } + + validationEnvironment = + validationEnvironment.transform(b -> b.context(GraphQLDirective.class, directive)); + // + // now run the directive rule with this directive instance + List ruleErrors = this.runConstraint(validationEnvironment); + errors.addAll(ruleErrors); + } + return errors; + } + + /** + * Returns true of the input type is one of the specified scalar types, regardless of non null + * ness + * + * @param inputType the type to check + * @param scalarTypes the array of scalar types + * + * @return true ifits oneof them + */ + protected boolean isOneOfTheseTypes(GraphQLInputType inputType, + GraphQLScalarType... scalarTypes) { + GraphQLInputType unwrappedType = Util.unwrapNonNull(inputType); + for (GraphQLScalarType scalarType : scalarTypes) { + if (unwrappedType.getName().equals(scalarType.getName())) { + return true; + } + } + return false; + } + + /** + * Returns an integer argument from a directive (or its default) and throws an assertion of the + * argument is null + * + * @param directive the directive to check + * @param argName the argument name + * + * @return a non null value + */ + protected int getIntArg(GraphQLDirective directive, String argName) { + GraphQLArgument argument = directive.getArgument(argName); + if (argument == null) { + return assertExpectedArgType(argName, "Int"); + } + Number value = (Number) argument.getValue(); + if (value == null) { + value = (Number) argument.getDefaultValue(); + if (value == null) { + return assertExpectedArgType(argName, "Int"); + } + } + return value.intValue(); + } + + /** + * Returns an String argument from a directive (or its default) and throws an assertion of the + * argument is null + * + * @param directive the directive to check + * @param argName the argument name + * + * @return a non null value + */ + protected String getStrArg(GraphQLDirective directive, String argName) { + GraphQLArgument argument = directive.getArgument(argName); + if (argument == null) { + return assertExpectedArgType(argName, "String"); + } + String value = (String) argument.getValue(); + if (value == null) { + value = (String) argument.getDefaultValue(); + if (value == null) { + return assertExpectedArgType(argName, "String"); + } + } + return value; + } + + /** + * Returns an boolean argument from a directive (or its default) and throws an assertion of the + * argument is null + * + * @param directive the directive to check + * @param argName the argument name + * + * @return a non null value + */ + protected boolean getBoolArg(GraphQLDirective directive, String argName) { + GraphQLArgument argument = directive.getArgument(argName); + if (argument == null) { + return assertExpectedArgType(argName, "Boolean"); + } + Object value = argument.getValue(); + if (value == null) { + value = argument.getDefaultValue(); + if (value == null) { + return assertExpectedArgType(argName, "Boolean"); + } + } + return Boolean.parseBoolean(String.valueOf(value)); + } + + /** + * Returns the "message : String" argument from a directive or makes up one called + * "graphql.validation.{name}.message" + * + * @param directive the directive to check + * + * @return a non null value + */ + protected String getMessageTemplate(GraphQLDirective directive) { + String msg = null; + GraphQLArgument arg = directive.getArgument("message"); + if (arg != null) { + msg = (String) arg.getValue(); + if (msg == null) { + msg = (String) arg.getDefaultValue(); + } + } + if (msg == null) { + msg = "graphql.validation." + getName() + ".message"; + } + return msg; + } + + /** + * Creates a map of named parameters for message interpolation + * + * @param validatedValue the value being validated + * @param validationEnvironment the validation environment + * @param args must be an key / value array with String keys as the even params and values as then + * odd params + * + * @return a map of message parameters + */ + protected Map mkMessageParams(Object validatedValue, + ValidationEnvironment validationEnvironment, Object... args) { + Map params = new LinkedHashMap<>(); + params.put("validatedValue", validatedValue); + params.put("constraint", getName()); + params.put("path", validationEnvironment.getValidatedPath()); + + params.putAll(mkMap(args)); + return params; + } + + /** + * Creates a new {@link graphql.GraphQLError} + * + * @param validationEnvironment the current validation environment + * @param directive the directive being run + * @param msgParams the map of parameters + * + * @return a list of a single error + */ + protected List mkError(ValidationEnvironment validationEnvironment, + GraphQLDirective directive, Map msgParams) { + String messageTemplate = getMessageTemplate(directive); + GraphQLError error = validationEnvironment.getInterpolator().interpolate(messageTemplate, + msgParams, validationEnvironment); + return singletonList(error); + } + + /** + * Return true if the type is a String or List type or + * {@link graphql.schema.GraphQLInputObjectType}, regardless of non null ness + * + * @param inputType the type to check + * + * @return true if one of the above + */ + protected boolean isStringOrListOrMap(GraphQLInputType inputType) { + GraphQLInputType unwrappedType = Util.unwrapOneAndAllNonNull(inputType); + return Scalars.GraphQLString.equals(unwrappedType) || isList(inputType) + || (unwrappedType instanceof GraphQLInputObjectType) + || (unwrappedType instanceof GraphQLTypeReference); + } + + /** + * Casts the object as a Map with an assertion of it is not one + * + * @param value the object to turn into a map + * + * @return a Map + */ + @SuppressWarnings("ConstantConditions") + protected Map asMap(Object value) { + Assert.assertTrue(value instanceof Map, "The argument value MUST be a Map value"); + return (Map) value; + } + + /** + * Makes the object a BigDecimal with an assertion if we have no conversion of it + * + * @param value the object to turn into a BigDecimal + * + * @return a BigDecimal + */ + protected BigDecimal asBigDecimal(Object value) throws NumberFormatException { + if (value == null) { + return Assert.assertShouldNeverHappen("Validation cant handle null objects BigDecimals"); + } + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + String bdStr = ""; + if (value instanceof Number) { + bdStr = value.toString(); + } else if (value instanceof String) { + bdStr = value.toString(); + } else { + Assert.assertShouldNeverHappen("Validation cant handle objects of type '%s' as BigDecimals", + value.getClass().getSimpleName()); + } + return new BigDecimal(bdStr); + } + + /** + * Makes the object a boolean with an assertion if we have no conversion of it + * + * @param value the boolean object + * + * @return a boolean + */ + protected boolean asBoolean(Object value) { + if (value == null) { + return Assert.assertShouldNeverHappen("Validation cant handle null objects Booleans"); + } + if (value instanceof Boolean) { + return (Boolean) value; + } else { + return Assert.assertShouldNeverHappen( + "Validation cant handle objects of type '%s' as Booleans", + value.getClass().getSimpleName()); + } + } + + /** + * Returns the length of a String of the size of a list or size of a Map + * + * @param inputType the input type + * @param value the value + * + * @return the length of a String or Map or List + */ + protected int getStringOrObjectOrMapLength(GraphQLInputType inputType, Object value) { + int valLen; + if (value == null) { + valLen = 0; + } else if (Scalars.GraphQLString.equals(Util.unwrapNonNull(inputType))) { + valLen = String.valueOf(value).length(); + } else if (isList(inputType)) { + valLen = getListLength(value); + } else { + valLen = getObjectLen(value); + } + return valLen; + } + + private int getObjectLen(Object value) { + if (value == null) { + return 0; + } + Map map = asMap(value); + return map.size(); + } + + private int getListLength(Object value) { + if (value instanceof Collection) { + return ((Collection) value).size(); + } else if (value instanceof Iterable) { + int len = 0; + for (Object ignored : ((Iterable) value)) { + len++; + } + return len; + } else if (value != null && value.getClass().isArray()) { + return Array.getLength(value); + } + return 0; + } + + private T assertExpectedArgType(String argName, String typeName) { + return Assert.assertShouldNeverHappen( + "A validation directive MUST have a '%s' argument of type '%s' with a default value", + argName, typeName); + } } diff --git a/src/main/java/graphql/validation/rules/TargetedValidationRules.java b/src/main/java/graphql/validation/rules/TargetedValidationRules.java index 9e94e22..934d9b4 100644 --- a/src/main/java/graphql/validation/rules/TargetedValidationRules.java +++ b/src/main/java/graphql/validation/rules/TargetedValidationRules.java @@ -2,15 +2,12 @@ import static graphql.validation.rules.ValidationEnvironment.ValidatedElement.ARGUMENT; import static graphql.validation.rules.ValidationEnvironment.ValidatedElement.FIELD; -import static graphql.validation.rules.ValidationEnvironment.ValidatedElement.INPUT_OBJECT_FIELD; - import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; - import graphql.Assert; import graphql.GraphQLError; import graphql.PublicApi; @@ -29,225 +26,237 @@ import graphql.util.FpKit; import graphql.validation.interpolation.MessageInterpolator; import graphql.validation.locale.LocaleUtil; +import graphql.validation.rules.ValidationEnvironment.ValidatedElement; import graphql.validation.util.Util; /** - * TargetedValidationRules is a holder of {@link graphql.validation.rules.ValidationRule}s targeted against a specific - * type, field and possible argument via {@link ValidationCoordinates}. It then allows those rules - * to be run against the specific fields based on runtime execution during {@link graphql.schema.DataFetcher} - * invocations. + * TargetedValidationRules is a holder of {@link graphql.validation.rules.ValidationRule}s targeted + * against a specific type, field and possible argument via {@link ValidationCoordinates}. It then + * allows those rules to be run against the specific fields based on runtime execution during + * {@link graphql.schema.DataFetcher} invocations. */ @PublicApi public class TargetedValidationRules { - private final ValidationRules validationRules; - - private final Map> rulesMap; - - public TargetedValidationRules(Builder builder) { - this.rulesMap = new HashMap<>(builder.rulesMap); - this.validationRules=builder.validationRules; + private final ValidationRules validationRules; + + private final Map> rulesMap; + + public TargetedValidationRules(Builder builder) { + this.rulesMap = new HashMap<>(builder.rulesMap); + this.validationRules = builder.validationRules; + } + + public static Builder newValidationRules(ValidationRules validationRules) { + return new Builder(validationRules); + } + + public boolean isEmpty() { + return rulesMap.isEmpty(); + } + + /** + * Runs the contained rules that match the currently executing field named by the + * {@link graphql.schema.DataFetchingEnvironment} + * + * @param env the field being executed + * @param interpolator the message interpolator to use + * @param defaultLocale the default locale in play + * + * @return a list of zero or more input data validation errors + */ + public List runValidationRules(DataFetchingEnvironment env, + MessageInterpolator interpolator, Locale defaultLocale) { + + defaultLocale = LocaleUtil.determineLocale(env, defaultLocale); + + List errors = new ArrayList<>(); + + GraphQLObjectType fieldContainer = env.getExecutionStepInfo().getFieldContainer(); + GraphQLFieldDefinition fieldDefinition = env.getFieldDefinition(); + ExecutionPath fieldPath = env.getExecutionStepInfo().getPath(); + // + // run the field specific rules + ValidationCoordinates fieldCoords = + ValidationCoordinates.newCoordinates(fieldContainer, fieldDefinition); + List rules = rulesMap.getOrDefault(fieldCoords, Collections.emptyList()); + if (!rules.isEmpty()) { + ValidationEnvironment ruleEnvironment = ValidationEnvironment.newValidationEnvironment() + .dataFetchingEnvironment(env).messageInterpolator(interpolator).locale(defaultLocale) + .validatedElement(FIELD).validatedPath(fieldPath).build(); + + for (ValidationRule rule : rules) { + List ruleErrors = rule.runValidation(ruleEnvironment); + errors.addAll(ruleErrors); + } + } + // + // run the argument specific rules next + List sortedArgs = + Util.sort(fieldDefinition.getArguments(), GraphQLArgument::getName); + for (GraphQLArgument fieldArg : sortedArgs) { + + ValidationCoordinates argCoords = + ValidationCoordinates.newCoordinates(fieldContainer, fieldDefinition, fieldArg); + + rules = rulesMap.getOrDefault(argCoords, Collections.emptyList()); + if (rules.isEmpty()) { + continue; + } + + Object argValue = env.getArgument(fieldArg.getName()); + GraphQLInputType inputType = fieldArg.getType(); + + ValidationEnvironment ruleEnvironment = ValidationEnvironment.newValidationEnvironment() + .dataFetchingEnvironment(env).argument(fieldArg).validatedElement(ARGUMENT) + .graphQLSchema(env.getGraphQLSchema()).validatedType(inputType).validatedValue(argValue) + .validatedPath(fieldPath.segment(fieldArg.getName())).directives(fieldArg.getDirectives()) + .messageInterpolator(interpolator).locale(defaultLocale).build(); + + errors.addAll(runValidationImpl(rules, ruleEnvironment, inputType, argValue, null)); } - public static Builder newValidationRules(ValidationRules validationRules) { - return new Builder(validationRules); + return errors; + } + + @SuppressWarnings("unchecked") + private List runValidationImpl(List rules, + ValidationEnvironment validationEnvironment, GraphQLInputType inputType, + Object validatedValue, GraphQLInputObjectType parentInputType) { + List errors = new ArrayList(); + for (ValidationRule rule : rules) { + errors.addAll(rule.runValidation(validationEnvironment)); } - public boolean isEmpty() { - return rulesMap.isEmpty(); + if (validatedValue == null) { + return errors; } - /** - * Runs the contained rules that match the currently executing field named by the {@link graphql.schema.DataFetchingEnvironment} - * - * @param env the field being executed - * @param interpolator the message interpolator to use - * @param defaultLocale the default locale in play - * - * @return a list of zero or more input data validation errors - */ - public List runValidationRules(DataFetchingEnvironment env, MessageInterpolator interpolator, Locale defaultLocale) { - - defaultLocale = LocaleUtil.determineLocale(env, defaultLocale); - - List errors = new ArrayList<>(); - - GraphQLObjectType fieldContainer = env.getExecutionStepInfo().getFieldContainer(); - GraphQLFieldDefinition fieldDefinition = env.getFieldDefinition(); - ExecutionPath fieldPath = env.getExecutionStepInfo().getPath(); - // - // run the field specific rules - ValidationCoordinates fieldCoords = ValidationCoordinates.newCoordinates(fieldContainer, fieldDefinition); - List rules = rulesMap.getOrDefault(fieldCoords, Collections.emptyList()); - if (!rules.isEmpty()) { - ValidationEnvironment ruleEnvironment = ValidationEnvironment.newValidationEnvironment() - .dataFetchingEnvironment(env) - .messageInterpolator(interpolator) - .locale(defaultLocale) - .validatedElement(FIELD) - .validatedPath(fieldPath) - .build(); - - for (ValidationRule rule : rules) { - List ruleErrors = rule.runValidation(ruleEnvironment); - errors.addAll(ruleErrors); - } - } - // - // run the argument specific rules next - List sortedArgs = Util.sort(fieldDefinition.getArguments(), GraphQLArgument::getName); - for (GraphQLArgument fieldArg : sortedArgs) { - - ValidationCoordinates argCoords = ValidationCoordinates.newCoordinates(fieldContainer, fieldDefinition, fieldArg); - - rules = rulesMap.getOrDefault(argCoords, Collections.emptyList()); - if (rules.isEmpty()) { - continue; - } - - Object argValue = env.getArgument(fieldArg.getName()); - GraphQLInputType inputType = fieldArg.getType(); - - ValidationEnvironment ruleEnvironment = ValidationEnvironment.newValidationEnvironment() - .dataFetchingEnvironment(env) - .argument(fieldArg) - .validatedElement(ARGUMENT) - .validatedType(inputType) - .validatedValue(argValue) - .validatedPath(fieldPath.segment(fieldArg.getName())) - .directives(fieldArg.getDirectives()) - .messageInterpolator(interpolator) - .locale(defaultLocale) - .build(); - - errors.addAll(runValidationImpl(rules, ruleEnvironment, inputType, argValue)); - } + inputType = (GraphQLInputType) GraphQLTypeUtil.unwrapNonNull(inputType); - return errors; - } - @SuppressWarnings("unchecked") - private List runValidationImpl(List rules, ValidationEnvironment validationEnvironment, GraphQLInputType inputType, Object validatedValue) { - List errors = new ArrayList(); - for(ValidationRule rule : rules) { - errors.addAll(rule.runValidation(validationEnvironment)); - } - - if (validatedValue == null) { - return errors; - } + if (GraphQLTypeUtil.isList(inputType)) { + List values = new ArrayList<>(FpKit.toCollection(validatedValue)); + List ruleErrors = walkListArg(rules, validationEnvironment, + (GraphQLList) inputType, values, parentInputType); + errors.addAll(ruleErrors); + } - inputType = (GraphQLInputType) GraphQLTypeUtil.unwrapNonNull(inputType); - - if (GraphQLTypeUtil.isList(inputType)) { - List values = new ArrayList<>(FpKit.toCollection(validatedValue)); - List ruleErrors = walkListArg(rules, validationEnvironment, (GraphQLList) inputType, values); - errors.addAll(ruleErrors); - } - - - if (inputType instanceof GraphQLInputObjectType) { - if (validatedValue instanceof Map) { - Map objectValue = (Map) validatedValue; - List ruleErrors = walkObjectArg(validationEnvironment, (GraphQLInputObjectType) inputType, objectValue); - errors.addAll(ruleErrors); - } else { - Assert.assertShouldNeverHappen("How can there be a `input` object type '%s' that does not have a matching Map java value", GraphQLTypeUtil.simplePrint(inputType)); - } - } - return errors; + if (inputType instanceof GraphQLInputObjectType) { + if (validatedValue instanceof Map) { + Map objectValue = (Map) validatedValue; + List ruleErrors = + walkObjectArg(validationEnvironment, (GraphQLInputObjectType) inputType, objectValue); + errors.addAll(ruleErrors); + } else { + Assert.assertShouldNeverHappen( + "How can there be a `input` object type '%s' that does not have a matching Map java value", + GraphQLTypeUtil.simplePrint(inputType)); + } } + return errors; + } - private List walkObjectArg(ValidationEnvironment validationEnvironment, GraphQLInputObjectType argumentType, Map objectMap) { - List errors = new ArrayList<>(); + private List walkObjectArg(ValidationEnvironment validationEnvironment, + GraphQLInputObjectType argumentType, Map objectMap) { + List errors = new ArrayList<>(); - // run them in a stable order - List fieldDefinitions = Util.sort(argumentType.getFieldDefinitions(), GraphQLInputObjectField::getName); - for (GraphQLInputObjectField inputField : fieldDefinitions) { + // run them in a stable order + List fieldDefinitions = + Util.sort(argumentType.getFieldDefinitions(), GraphQLInputObjectField::getName); + for (GraphQLInputObjectField inputField : fieldDefinitions) { + Object validatedValue = + objectMap.getOrDefault(inputField.getName(), inputField.getDefaultValue()); + if (validatedValue == null) { + continue; + } - GraphQLInputType fieldType = inputField.getType(); - Object validatedValue = objectMap.getOrDefault(inputField.getName(), inputField.getDefaultValue()); - if (validatedValue == null) { - continue; - } + ExecutionPath newPath = + validationEnvironment.getValidatedPath().segment(inputField.getName()); + GraphQLInputObjectField fieldDef = validationEnvironment.getGraphQLSchema().getCodeRegistry() + .getFieldVisibility().getFieldDefinition(argumentType, inputField.getName()); - ExecutionPath newPath = validationEnvironment.getValidatedPath().segment(inputField.getName()); + ValidationEnvironment newValidationEnvironment = validationEnvironment + .transform(builder -> builder.validatedPath(newPath).validatedValue(validatedValue) + .validatedType(inputField.getType()).directives(fieldDef.getDirectives()) + .validatedElement(ValidatedElement.INPUT_OBJECT_FIELD)); - ValidationEnvironment newValidationEnvironment = validationEnvironment.transform(builder -> builder - .validatedPath(newPath) - .validatedValue(validatedValue) - .validatedType(fieldType) - .directives(inputField.getDirectives()) - .validatedElement(INPUT_OBJECT_FIELD) - ); - - List rulesChild = validationRules.getRulesFor(newValidationEnvironment.getArgument(), newValidationEnvironment.getFieldDefinition(), newValidationEnvironment.getFieldsContainer()); - errors.addAll(runValidationImpl(rulesChild, newValidationEnvironment, fieldType, validatedValue)); - } - return errors; - } + List rulesChild = validationRules.getRulesFor( + newValidationEnvironment.getArgument(), newValidationEnvironment.getFieldDefinition(), + newValidationEnvironment.getFieldsContainer()); - private List walkListArg(List rules, ValidationEnvironment validationEnvironment, GraphQLList argumentType, List objectList) { - List errors = new ArrayList<>(); + errors.addAll(runValidationImpl(rulesChild, newValidationEnvironment, inputField.getType(), + validatedValue, argumentType)); - GraphQLInputType listItemType = Util.unwrapOneAndAllNonNull(argumentType); - List directives; - if (!(listItemType instanceof GraphQLDirectiveContainer)) { - directives = Collections.emptyList(); - } else { - directives = ((GraphQLDirectiveContainer) listItemType).getDirectives(); - } - int ix = 0; - for (Object value : objectList) { + } + return errors; + } + + private List walkListArg(List rules, + ValidationEnvironment validationEnvironment, GraphQLList argumentType, + List objectList, GraphQLInputObjectType parentInputType) { + List errors = new ArrayList<>(); + + GraphQLInputType listItemType = Util.unwrapOneAndAllNonNull(argumentType); + List directives; + if (!(listItemType instanceof GraphQLDirectiveContainer)) { + directives = Collections.emptyList(); + } else { + directives = validationEnvironment.getGraphQLSchema().getCodeRegistry().getFieldVisibility() + .getFieldDefinition(parentInputType, + validationEnvironment.getValidatedPath().getSegmentName()) + .getDirectives(); + } - ExecutionPath newPath = validationEnvironment.getValidatedPath().segment(ix); + int ix = 0; + for (Object value : objectList) { - ValidationEnvironment newValidationEnvironment = validationEnvironment.transform(builder -> builder - .validatedPath(newPath) - .validatedValue(value) - .validatedType(listItemType) - .directives(directives) - ); + ExecutionPath newPath = validationEnvironment.getValidatedPath().segment(ix); - List ruleErrors = runValidationImpl(rules, newValidationEnvironment, listItemType, value); - errors.addAll(ruleErrors); - ix++; - } - return errors; + ValidationEnvironment newValidationEnvironment = + validationEnvironment.transform(builder -> builder.validatedPath(newPath) + .validatedValue(value).validatedType(listItemType).directives(directives)); + + List ruleErrors = + runValidationImpl(rules, newValidationEnvironment, listItemType, value, parentInputType); + errors.addAll(ruleErrors); + ix++; } + return errors; + } - public static class Builder { - ValidationRules validationRules; - Map> rulesMap = new HashMap<>(); + public static class Builder { + ValidationRules validationRules; + Map> rulesMap = new HashMap<>(); - public Builder(ValidationRules validationRules) { - this.validationRules=validationRules; - } - - public Builder addRule(ValidationCoordinates coordinates, ValidationRule rule) { - rulesMap.compute(coordinates, (key, listOfRules) -> { - if (listOfRules == null) { - listOfRules = new ArrayList<>(); - } - listOfRules.add(rule); - return listOfRules; - }); - return this; - } + public Builder(ValidationRules validationRules) { + this.validationRules = validationRules; + } - public Builder addRules(ValidationCoordinates argCoords, List rules) { - for (ValidationRule rule : rules) { - addRule(argCoords, rule); - } - return this; + public Builder addRule(ValidationCoordinates coordinates, ValidationRule rule) { + rulesMap.compute(coordinates, (key, listOfRules) -> { + if (listOfRules == null) { + listOfRules = new ArrayList<>(); } + listOfRules.add(rule); + return listOfRules; + }); + return this; + } - public TargetedValidationRules build() { - return new TargetedValidationRules(this); - } + public Builder addRules(ValidationCoordinates argCoords, List rules) { + for (ValidationRule rule : rules) { + addRule(argCoords, rule); + } + return this; + } + + public TargetedValidationRules build() { + return new TargetedValidationRules(this); } + } } diff --git a/src/main/java/graphql/validation/rules/ValidationEnvironment.java b/src/main/java/graphql/validation/rules/ValidationEnvironment.java index c7db37f..bf07541 100644 --- a/src/main/java/graphql/validation/rules/ValidationEnvironment.java +++ b/src/main/java/graphql/validation/rules/ValidationEnvironment.java @@ -1,5 +1,11 @@ package graphql.validation.rules; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Consumer; import graphql.PublicApi; import graphql.execution.ExecutionPath; import graphql.language.SourceLocation; @@ -9,256 +15,263 @@ import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLFieldsContainer; import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLSchema; import graphql.validation.interpolation.MessageInterpolator; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.function.Consumer; - /** * The environment in which validation runs */ @PublicApi public class ValidationEnvironment { + /** + * The type of element being validated + */ + public enum ValidatedElement { /** - * The type of element being validated + * A output field is being validated */ - public enum ValidatedElement { - /** - * A output field is being validated - */ - FIELD, - /** - * An argument on a graphql output field is being validated - */ - ARGUMENT, - /** - * A input type field is being validated - */ - INPUT_OBJECT_FIELD - } - - private final GraphQLFieldsContainer fieldsContainer; - private final GraphQLFieldDefinition fieldDefinition; - private final GraphQLArgument argument; - private final ExecutionPath executionPath; - private final ExecutionPath validatedPath; - private final SourceLocation location; - private final MessageInterpolator interpolator; - private final Map contextMap; - private final Locale locale; - private final Map argumentValues; - private final Object validatedValue; - private final GraphQLInputType validatedType; - private final ValidatedElement validatedElement; - private final List directives; - - private ValidationEnvironment(Builder builder) { - this.argument = builder.argument; - this.argumentValues = Collections.unmodifiableMap(builder.argumentValues); - this.contextMap = Collections.unmodifiableMap(builder.contextMap); - this.fieldDefinition = builder.fieldDefinition; - this.executionPath = builder.executionPath; - this.validatedPath = builder.validatedPath; - this.validatedType = builder.validatedType; - this.fieldsContainer = builder.fieldsContainer; - this.interpolator = builder.interpolator; - this.locale = builder.locale; - this.location = builder.location; - this.validatedValue = builder.validatedValue; - this.validatedElement = builder.validatedElement; - this.directives = builder.directives; + FIELD, + /** + * An argument on a graphql output field is being validated + */ + ARGUMENT, + /** + * A input type field is being validated + */ + INPUT_OBJECT_FIELD + } + + private final GraphQLFieldsContainer fieldsContainer; + private final GraphQLFieldDefinition fieldDefinition; + private final GraphQLArgument argument; + private final ExecutionPath executionPath; + private final ExecutionPath validatedPath; + private final SourceLocation location; + private final MessageInterpolator interpolator; + private final Map contextMap; + private final Locale locale; + private final Map argumentValues; + private final Object validatedValue; + private final GraphQLInputType validatedType; + private final ValidatedElement validatedElement; + private final List directives; + private final GraphQLSchema graphQLSchema; + + private ValidationEnvironment(Builder builder) { + this.argument = builder.argument; + this.argumentValues = Collections.unmodifiableMap(builder.argumentValues); + this.contextMap = Collections.unmodifiableMap(builder.contextMap); + this.fieldDefinition = builder.fieldDefinition; + this.executionPath = builder.executionPath; + this.validatedPath = builder.validatedPath; + this.validatedType = builder.validatedType; + this.fieldsContainer = builder.fieldsContainer; + this.interpolator = builder.interpolator; + this.locale = builder.locale; + this.location = builder.location; + this.validatedValue = builder.validatedValue; + this.validatedElement = builder.validatedElement; + this.directives = builder.directives; + this.graphQLSchema = builder.graphQLSchema; + } + + public static Builder newValidationEnvironment() { + return new Builder(); + } + + @SuppressWarnings("unchecked") + public T getContextObject(Class clazz, Object... defaultVal) { + return (T) contextMap.getOrDefault(clazz, defaultVal.length == 0 ? null : defaultVal[0]); + } + + public GraphQLFieldsContainer getFieldsContainer() { + return fieldsContainer; + } + + public GraphQLFieldDefinition getFieldDefinition() { + return fieldDefinition; + } + + public GraphQLArgument getArgument() { + return argument; + } + + public SourceLocation getLocation() { + return location; + } + + public ExecutionPath getValidatedPath() { + return validatedPath; + } + + public ExecutionPath getExecutionPath() { + return executionPath; + } + + public GraphQLInputType getValidatedType() { + return validatedType; + } + + public Object getValidatedValue() { + return validatedValue; + } + + public Map getArgumentValues() { + return argumentValues; + } + + public MessageInterpolator getInterpolator() { + return interpolator; + } + + public Locale getLocale() { + return locale; + } + + public ValidatedElement getValidatedElement() { + return validatedElement; + } + + public List getDirectives() { + return directives; + } + + public GraphQLSchema getGraphQLSchema() { + return graphQLSchema; + } + + public ValidationEnvironment transform(Consumer builderConsumer) { + Builder builder = newValidationEnvironment().validationEnvironment(this); + builderConsumer.accept(builder); + return builder.build(); + } + + public static class Builder { + private final Map contextMap = new HashMap<>(); + private GraphQLArgument argument; + private Map argumentValues = new HashMap<>(); + private GraphQLFieldDefinition fieldDefinition; + private ExecutionPath validatedPath = ExecutionPath.rootPath(); + private ExecutionPath executionPath; + private GraphQLFieldsContainer fieldsContainer; + private MessageInterpolator interpolator; + private Locale locale; + private SourceLocation location; + private Object validatedValue; + private GraphQLInputType validatedType; + private ValidatedElement validatedElement; + private List directives = Collections.emptyList(); + private GraphQLSchema graphQLSchema; + + public Builder validationEnvironment(ValidationEnvironment validationEnvironment) { + this.argument = validationEnvironment.argument; + this.argumentValues = validationEnvironment.argumentValues; + this.contextMap.putAll(validationEnvironment.contextMap); + this.fieldDefinition = validationEnvironment.fieldDefinition; + this.executionPath = validationEnvironment.executionPath; + this.validatedPath = validationEnvironment.validatedPath; + this.validatedType = validationEnvironment.validatedType; + this.fieldsContainer = validationEnvironment.fieldsContainer; + this.interpolator = validationEnvironment.interpolator; + this.locale = validationEnvironment.locale; + this.location = validationEnvironment.location; + this.validatedValue = validationEnvironment.validatedValue; + this.validatedElement = validationEnvironment.validatedElement; + this.directives = validationEnvironment.directives; + this.graphQLSchema = validationEnvironment.graphQLSchema; + return this; } - public static Builder newValidationEnvironment() { - return new Builder(); + public Builder dataFetchingEnvironment(DataFetchingEnvironment dataFetchingEnvironment) { + fieldsContainer(dataFetchingEnvironment.getExecutionStepInfo().getFieldContainer()); + fieldDefinition(dataFetchingEnvironment.getFieldDefinition()); + directives(dataFetchingEnvironment.getFieldDefinition().getDirectives()); + executionPath(dataFetchingEnvironment.getExecutionStepInfo().getPath()); + validatedPath(dataFetchingEnvironment.getExecutionStepInfo().getPath()); + location(dataFetchingEnvironment.getField().getSourceLocation()); + argumentValues(dataFetchingEnvironment.getArguments()); + validatedElement(ValidatedElement.FIELD); + return this; } - @SuppressWarnings("unchecked") - public T getContextObject(Class clazz, Object... defaultVal) { - return (T) contextMap.getOrDefault(clazz, defaultVal.length == 0 ? null : defaultVal[0]); + public Builder argument(GraphQLArgument argument) { + this.argument = argument; + return this; } - public GraphQLFieldsContainer getFieldsContainer() { - return fieldsContainer; + public Builder context(Class clazz, Object value) { + this.contextMap.put(clazz, value); + return this; } - public GraphQLFieldDefinition getFieldDefinition() { - return fieldDefinition; + public Builder fieldsContainer(GraphQLFieldsContainer fieldsContainer) { + this.fieldsContainer = fieldsContainer; + return this; } - public GraphQLArgument getArgument() { - return argument; + public Builder executionPath(ExecutionPath executionPath) { + this.executionPath = executionPath; + return this; } - public SourceLocation getLocation() { - return location; + public Builder fieldDefinition(GraphQLFieldDefinition fieldDefinition) { + this.fieldDefinition = fieldDefinition; + return this; } - public ExecutionPath getValidatedPath() { - return validatedPath; + public Builder validatedElement(ValidatedElement validatedElement) { + this.validatedElement = validatedElement; + return this; } - public ExecutionPath getExecutionPath() { - return executionPath; + public Builder validatedType(GraphQLInputType validatedType) { + this.validatedType = validatedType; + return this; } - public GraphQLInputType getValidatedType() { - return validatedType; + public Builder validatedValue(Object validatedValue) { + this.validatedValue = validatedValue; + return this; } - public Object getValidatedValue() { - return validatedValue; + public Builder validatedPath(ExecutionPath validatedPath) { + this.validatedPath = validatedPath; + return this; } - public Map getArgumentValues() { - return argumentValues; + public Builder argumentValues(Map argumentValues) { + this.argumentValues = argumentValues; + return this; } - public MessageInterpolator getInterpolator() { - return interpolator; + public Builder location(SourceLocation location) { + this.location = location; + return this; } - public Locale getLocale() { - return locale; + public Builder messageInterpolator(MessageInterpolator interpolator) { + this.interpolator = interpolator; + return this; } - public ValidatedElement getValidatedElement() { - return validatedElement; + public Builder locale(Locale locale) { + this.locale = locale; + return this; } - public List getDirectives() { - return directives; + public Builder directives(List directives) { + this.directives = directives; + return this; } - public ValidationEnvironment transform(Consumer builderConsumer) { - Builder builder = newValidationEnvironment().validationEnvironment(this); - builderConsumer.accept(builder); - return builder.build(); + public Builder graphQLSchema(GraphQLSchema graphQLSchema) { + this.graphQLSchema = graphQLSchema; + return this; } - public static class Builder { - private final Map contextMap = new HashMap<>(); - private GraphQLArgument argument; - private Map argumentValues = new HashMap<>(); - private GraphQLFieldDefinition fieldDefinition; - private ExecutionPath validatedPath = ExecutionPath.rootPath(); - private ExecutionPath executionPath; - private GraphQLFieldsContainer fieldsContainer; - private MessageInterpolator interpolator; - private Locale locale; - private SourceLocation location; - private Object validatedValue; - private GraphQLInputType validatedType; - private ValidatedElement validatedElement; - private List directives = Collections.emptyList(); - - public Builder validationEnvironment(ValidationEnvironment validationEnvironment) { - this.argument = validationEnvironment.argument; - this.argumentValues = validationEnvironment.argumentValues; - this.contextMap.putAll(validationEnvironment.contextMap); - this.fieldDefinition = validationEnvironment.fieldDefinition; - this.executionPath = validationEnvironment.executionPath; - this.validatedPath = validationEnvironment.validatedPath; - this.validatedType = validationEnvironment.validatedType; - this.fieldsContainer = validationEnvironment.fieldsContainer; - this.interpolator = validationEnvironment.interpolator; - this.locale = validationEnvironment.locale; - this.location = validationEnvironment.location; - this.validatedValue = validationEnvironment.validatedValue; - this.validatedElement = validationEnvironment.validatedElement; - this.directives = validationEnvironment.directives; - return this; - } - - public Builder dataFetchingEnvironment(DataFetchingEnvironment dataFetchingEnvironment) { - fieldsContainer(dataFetchingEnvironment.getExecutionStepInfo().getFieldContainer()); - fieldDefinition(dataFetchingEnvironment.getFieldDefinition()); - directives(dataFetchingEnvironment.getFieldDefinition().getDirectives()); - executionPath(dataFetchingEnvironment.getExecutionStepInfo().getPath()); - validatedPath(dataFetchingEnvironment.getExecutionStepInfo().getPath()); - location(dataFetchingEnvironment.getField().getSourceLocation()); - argumentValues(dataFetchingEnvironment.getArguments()); - validatedElement(ValidatedElement.FIELD); - return this; - } - - public Builder argument(GraphQLArgument argument) { - this.argument = argument; - return this; - } - - public Builder context(Class clazz, Object value) { - this.contextMap.put(clazz, value); - return this; - } - - public Builder fieldsContainer(GraphQLFieldsContainer fieldsContainer) { - this.fieldsContainer = fieldsContainer; - return this; - } - - public Builder executionPath(ExecutionPath executionPath) { - this.executionPath = executionPath; - return this; - } - - public Builder fieldDefinition(GraphQLFieldDefinition fieldDefinition) { - this.fieldDefinition = fieldDefinition; - return this; - } - - public Builder validatedElement(ValidatedElement validatedElement) { - this.validatedElement = validatedElement; - return this; - } - - public Builder validatedType(GraphQLInputType validatedType) { - this.validatedType = validatedType; - return this; - } - - public Builder validatedValue(Object validatedValue) { - this.validatedValue = validatedValue; - return this; - } - - public Builder validatedPath(ExecutionPath validatedPath) { - this.validatedPath = validatedPath; - return this; - } - - public Builder argumentValues(Map argumentValues) { - this.argumentValues = argumentValues; - return this; - } - - public Builder location(SourceLocation location) { - this.location = location; - return this; - } - - public Builder messageInterpolator(MessageInterpolator interpolator) { - this.interpolator = interpolator; - return this; - } - - public Builder locale(Locale locale) { - this.locale = locale; - return this; - } - - public Builder directives(List directives) { - this.directives = directives; - return this; - } - - public ValidationEnvironment build() { - return new ValidationEnvironment(this); - } + public ValidationEnvironment build() { + return new ValidationEnvironment(this); } + } }