Skip to content

Commit acd55e2

Browse files
committed
Better documentation
1 parent 8ebd3aa commit acd55e2

14 files changed

+177
-70
lines changed

readme.md

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ It has NOT yet been consumed by a production like project and hence its API usef
1717

1818
TODO
1919

20+
2021
# SDL @Directive constraints
2122

2223
This library a series of directives that can be applied to field arguments and input type fields which will constrain their allowable values.
@@ -49,8 +50,81 @@ For example
4950
In the example above, we have a `applications` argument that takes at most 10 applications and within each `Application` input object,
5051
the `name` field must be at least 3 characters long and no more than 100 characters long to be considered valid.
5152

53+
# Java Expression Language (Java EL)
54+
55+
The `@Expression` validation directive allows Java EL to be used to help build validation rules.
56+
57+
Java EL is a powerful expression syntax for expressing validation conditions.
58+
59+
Some simple sample Java EL expressions might be :
60+
61+
| EL Expression | Result |
62+
| ------------- | -------- |
63+
| `${1> (4/2)} ` | `false` |
64+
| `${4.0>= 3} ` | `true` |
65+
| `${100.0 == 100}` | `true` |
66+
| `${(10*10) ne 100} ` | `false` |
67+
| `${'a' > 'b'}` | `false` |
68+
| `${'hip' lt 'hit'}` | `true` |
69+
| `${4> 3} ` | `true` |
70+
| `${1.2E4 + 1.4} ` | `12001.4` |
71+
| `${3 div 4}` | `0.75` |
72+
| `${10 mod 4}` | `2` |
73+
| `${((x, y) → x + y)(3, 5.5)}` | `8.5` |
74+
| `[1,2,3,4].stream().sum()` | `10` |
75+
| `[1,3,5,2].stream().sorted().toList()` | `[1, 2, 3, 5]` |
76+
77+
The following validation variables are made available to you :
78+
79+
| Name | Value |
80+
| ------------- | ----- |
81+
| `validatedValue` | The value being validated |
82+
| `gqlField` | The `GraphQLFieldDefinition` being validated |
83+
| `gqlFieldContainer` | The `GraphQLFieldsContainer` parent type containing that field |
84+
| `gqlArgument` | The `GraphQLArgument` being validated. This can be null for field level validation |
85+
| `arguments` | The map of all argument values for the current field |
86+
| `args` | A short hand name for the map of all argument values for the current field |
87+
88+
The Java EL expression MUST evaluate to a boolean value to be useful in the `@Expresion` directive.
89+
90+
See here for [a more complete overview of Java EL](https://javaee.github.io/tutorial/jsf-el001.html)
91+
92+
93+
# Message Interpolation
94+
95+
The validation code uses a `graphql.validation.interpolation.MessageInterpolator` interface to build out error messages. A default
96+
`ResourceBundleMessageInterpolator` class is provided to load error messages from Java resource bundles to allow internationalised messages (I18N)
97+
98+
You can use Java EL syntax in the message templates to format even more powerful error messages.
99+
100+
```
101+
The field ${gqlField.name} has the following invalid value : ${formatter.format('%1$.2f', validatedValue)}
102+
```
103+
104+
If you use directive arguments like `message : String = "graphql.validation.Size.message"` then the `ResourceBundleMessageInterpolator` class
105+
will use that as a resource bundle lookup key. This too is inspired by the javax.validation annotations and how they work.
106+
107+
Like javax.validation, this library ships with some default error message templates but you can override them.
108+
109+
110+
# Complex input types
111+
112+
You can put @Directive constraints on complex input types as well as simple field arguments
113+
114+
```graphql
115+
input ProductItem {
116+
code : String @Size(max : 5)
117+
price : String @Size(max : 3)
118+
}
119+
120+
type Mutation {
121+
updateProduct( product : ID, items : [ProductItem]) : Product
122+
}
123+
```
124+
125+
In the example above each `ProductItem` in the list of items is examined for valid values
52126

53-
## The supplied constraints
127+
## The supplied @Directive constraints
54128

55129
<!-- generated by DocHelper on 2019-08-17T11:55:22.933Z -->
56130

src/main/java/graphql/validation/constraints/AbstractDirectiveConstraint.java

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import static graphql.schema.GraphQLTypeUtil.isList;
2828
import static graphql.validation.rules.ValidationEnvironment.ValidatedElement.FIELD;
29+
import static graphql.validation.util.Util.mkMap;
2930
import static java.util.Collections.singletonList;
3031

3132
@SuppressWarnings("UnnecessaryLocalVariable")
@@ -73,7 +74,7 @@ public boolean appliesTo(GraphQLArgument argument, GraphQLFieldDefinition fieldD
7374
// if they have a @Directive on there BUT it cant handle that type
7475
// then is a really bad situation
7576
String argType = GraphQLTypeUtil.simplePrint(inputType);
76-
Assert.assertShouldNeverHappen("The directive rule '%s' cannot be placed on elements of type '%s'", this.getName(), argType);
77+
Assert.assertTrue(false, "The directive rule '%s' cannot be placed on elements of type '%s'", "@" + this.getName(), argType);
7778
}
7879
return false;
7980
});
@@ -282,24 +283,6 @@ protected Map<String, Object> mkMessageParams(Object validatedValue, ValidationE
282283
}
283284

284285

285-
/**
286-
* Makes a map of the args
287-
*
288-
* @param args must be an key / value array with String keys as the even params and values as then odd params
289-
*
290-
* @return a map of the args
291-
*/
292-
protected Map<String, Object> mkMap(Object... args) {
293-
Map<String, Object> params = new LinkedHashMap<>();
294-
Assert.assertTrue(args.length % 2 == 0, "You MUST pass in an even number of arguments");
295-
for (int ix = 0; ix < args.length; ix = ix + 2) {
296-
Object key = args[ix];
297-
Assert.assertTrue(key instanceof String, "You MUST pass in a message param string key");
298-
Object val = args[ix + 1];
299-
params.put(String.valueOf(key), val);
300-
}
301-
return params;
302-
}
303286

304287
/**
305288
* Creates a new {@link graphql.GraphQLError}

src/main/java/graphql/validation/constraints/standard/ExpressionConstraint.java

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
import graphql.validation.constraints.AbstractDirectiveConstraint;
99
import graphql.validation.constraints.Documentation;
1010
import graphql.validation.el.ELSupport;
11+
import graphql.validation.el.StandardELVariables;
1112
import graphql.validation.rules.ValidationEnvironment;
1213

1314
import java.util.Collections;
1415
import java.util.List;
1516
import java.util.Map;
16-
import java.util.function.Supplier;
1717

1818
public class ExpressionConstraint extends AbstractDirectiveConstraint {
1919

@@ -54,25 +54,13 @@ public boolean appliesTo(GraphQLFieldDefinition fieldDefinition, GraphQLFieldsCo
5454

5555
@Override
5656
protected List<GraphQLError> runConstraint(ValidationEnvironment validationEnvironment) {
57-
GraphQLFieldDefinition fieldDefinition = validationEnvironment.getFieldDefinition();
5857
GraphQLDirective directive = validationEnvironment.getContextObject(GraphQLDirective.class);
5958
String expression = helpWithCurlyBraces(getStrArg(directive, "value"));
6059

61-
Object argument = validationEnvironment.getArgument();
62-
GraphQLFieldsContainer fieldsContainer = validationEnvironment.getFieldsContainer();
6360
Object validatedValue = validationEnvironment.getValidatedValue();
64-
Map<String, Object> argumentValues = validationEnvironment.getArgumentValues();
6561

66-
Map<String, Object> variables = mkMap(
67-
"validatedValue", validatedValue,
62+
Map<String, Object> variables = StandardELVariables.standardELVars(validationEnvironment);
6863

69-
"gqlField", fieldDefinition,
70-
"gqlFieldContainer", fieldsContainer,
71-
"gqlArgument", argument,
72-
73-
"args", argumentValues, // short hand
74-
"arguments", argumentValues
75-
);
7664
ELSupport elSupport = new ELSupport(validationEnvironment.getLocale());
7765
boolean isOK = elSupport.evaluateBoolean(expression, variables);
7866

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package graphql.validation.el;
2+
3+
import graphql.schema.GraphQLFieldDefinition;
4+
import graphql.schema.GraphQLFieldsContainer;
5+
import graphql.validation.rules.ValidationEnvironment;
6+
7+
import java.util.Map;
8+
9+
import static graphql.validation.util.Util.mkMap;
10+
11+
public class StandardELVariables {
12+
13+
public static Map<String, Object> standardELVars(ValidationEnvironment validationEnvironment) {
14+
GraphQLFieldDefinition fieldDefinition = validationEnvironment.getFieldDefinition();
15+
Object argument = validationEnvironment.getArgument();
16+
GraphQLFieldsContainer fieldsContainer = validationEnvironment.getFieldsContainer();
17+
Object validatedValue = validationEnvironment.getValidatedValue();
18+
Map<String, Object> argumentValues = validationEnvironment.getArgumentValues();
19+
20+
return mkMap(
21+
"validatedValue", validatedValue,
22+
23+
"gqlField", fieldDefinition,
24+
"gqlFieldContainer", fieldsContainer,
25+
26+
"gqlArgument", argument,
27+
28+
"args", argumentValues, // short hand
29+
"arguments", argumentValues
30+
);
31+
}
32+
}

src/main/java/graphql/validation/interpolation/MessageInterpolator.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88

99
/**
1010
* This is responsible for taking an message template and parameters
11-
* and turning it into a {@link graphql.GraphQLError}
11+
* and turning it into a {@link graphql.GraphQLError}.
12+
* <p>
13+
* Remember error messages are allow to use Java EL expressions, like <pre>{@code ${formatter.format('%1$.2f', validatedValue)}}</pre> to build
14+
* more powerful error messages.
1215
*/
1316
@PublicSpi
1417
public interface MessageInterpolator {
@@ -18,7 +21,6 @@ public interface MessageInterpolator {
1821
* @param messageTemplate the message template
1922
* @param messageParams the parameters to this error
2023
* @param validationEnvironment the validation environment
21-
*
2224
* @return a {@link graphql.GraphQLError}
2325
*/
2426
GraphQLError interpolate(String messageTemplate, Map<String, Object> messageParams, ValidationEnvironment validationEnvironment);

src/main/java/graphql/validation/interpolation/ResourceBundleMessageInterpolator.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import graphql.GraphqlErrorBuilder;
66
import graphql.execution.ExecutionPath;
77
import graphql.schema.GraphQLDirective;
8+
import graphql.validation.el.StandardELVariables;
89
import graphql.validation.rules.ValidationEnvironment;
910
import org.hibernate.validator.internal.engine.MessageInterpolatorContext;
1011
import org.hibernate.validator.internal.metadata.core.ConstraintHelper;
@@ -152,8 +153,7 @@ private MessageInterpolatorContext buildHibernateContext(Map<String, Object> mes
152153
annotationDescriptor, ElementType.FIELD
153154
);
154155

155-
// TODO - we should we put in here extra??
156-
Map<String, Object> expressionVariables = Collections.emptyMap();
156+
Map<String, Object> expressionVariables = StandardELVariables.standardELVars(validationEnvironment);
157157

158158
Class<?> rootBeanType = null;
159159
return new MessageInterpolatorContext(

src/main/java/graphql/validation/rules/OnValidationErrorStrategy.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
import java.util.List;
99

1010
/**
11-
* A callback that indicates whether to continue after errors and if not what value should be
12-
* returned.
11+
* A callback that indicates whether to continue the data fetching after validation errors are detected and what value should be
12+
* returned if it decides to not continue.
13+
* <p>
14+
* {@link #RETURN_NULL} is a common strategy to use, that is return null as the value for an invalid field
1315
*/
1416
@PublicSpi
1517
public interface OnValidationErrorStrategy {
@@ -35,7 +37,6 @@ public Object onErrorValue(List<GraphQLError> errors, DataFetchingEnvironment en
3537
*
3638
* @param errors the list errors
3739
* @param environment the environment in play
38-
*
3940
* @return true if the current data fetch should continue
4041
*/
4142
boolean shouldContinue(List<GraphQLError> errors, DataFetchingEnvironment environment);
@@ -45,7 +46,6 @@ public Object onErrorValue(List<GraphQLError> errors, DataFetchingEnvironment en
4546
*
4647
* @param errors the list errors
4748
* @param environment the environment in play
48-
*
4949
* @return an object (a sensible value would be null)
5050
*/
5151
Object onErrorValue(List<GraphQLError> errors, DataFetchingEnvironment environment);

src/main/java/graphql/validation/rules/PossibleValidationRules.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ public Builder messageInterpolator(MessageInterpolator messageInterpolator) {
123123
return this;
124124
}
125125

126+
/**
127+
* This sets the locale of the possible rules. This is only needed while graphql-java does not allow you to get the
128+
* locale from the {@link graphql.ExecutionInput}. A PR for this is in the works. Once that is available, then this method
129+
* will not be as useful.
130+
*
131+
* @param locale the locale to use for message interpolation
132+
* @return this builder
133+
*/
126134
public Builder locale(Locale locale) {
127135
this.locale = locale;
128136
return this;

src/main/java/graphql/validation/rules/ValidationCoordinates.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import java.util.Objects;
99
import java.util.StringJoiner;
1010

11+
/**
12+
* Validation rules can be co-ordinated on a field (within a fields container) or an argument on a field (within a fields container)
13+
*/
1114
@PublicApi
1215
public class ValidationCoordinates {
1316

src/main/java/graphql/validation/rules/ValidationRules.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232

3333
/**
3434
* ValidationRules is a holder of {@link graphql.validation.rules.ValidationRule}s against a specific
35-
* type, field and possible argument via {@link ValidationCoordinates}
35+
* type, field and possible argument via {@link ValidationCoordinates}. It then allows those rules
36+
* to be run against the specific fields based on runtime execution during {@link graphql.schema.DataFetcher}
37+
* invocations.
3638
*/
3739
@PublicApi
3840
public class ValidationRules {
@@ -51,6 +53,14 @@ public boolean isEmpty() {
5153
return rulesMap.isEmpty();
5254
}
5355

56+
/**
57+
* Runs the contained rules that match the currently executing field named by the {@link graphql.schema.DataFetchingEnvironment}
58+
*
59+
* @param env the field being executed
60+
* @param interpolator the message interpolator to use
61+
* @param locale the locale in play
62+
* @return a list of zero or more data validation errors
63+
*/
5464
public List<GraphQLError> runValidationRules(DataFetchingEnvironment env, MessageInterpolator interpolator, Locale locale) {
5565

5666
List<GraphQLError> errors = new ArrayList<>();

0 commit comments

Comments
 (0)