Skip to content

Commit 4b9da1d

Browse files
committed
Added early expression support
1 parent c3383bf commit 4b9da1d

File tree

13 files changed

+370
-51
lines changed

13 files changed

+370
-51
lines changed

readme.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ the `name` field must be at least 3 characters long and no more than 100 charact
4848

4949
## The supplied constraints
5050

51+
### @Arguments
52+
53+
The provided expression must evaluate to true.
54+
55+
- Example : `drivers( first : Int, after : String!, last : Int, before : String)
56+
: DriverConnection @Arguments(expression : "${args.containsOneOf('first','last') }"`
57+
58+
- Applies to : `Output Fields`
59+
60+
- SDL : `directive @Arguments(expression : String!, message : String = "graphql.validation.Arguments.message") on FIELD_DEFINITION`
61+
62+
- Message : `graphql.validation.Arguments.message`
63+
64+
5165
### @AssertFalse
5266

5367
The boolean value must be false.

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,19 @@ protected Map<String, Object> mkMessageParams(Object validatedValue, ValidationE
204204
params.put("constraint", getName());
205205
params.put("path", mkFieldOrArgPath(validationEnvironment));
206206

207+
params.putAll(mkMap(args));
208+
return params;
209+
}
210+
211+
/**
212+
* Makes a map of the args
213+
*
214+
* @param args must be an key / value array with String keys as the even params and values as then odd params
215+
*
216+
* @return a map of the args
217+
*/
218+
protected Map<String, Object> mkMap(Object... args) {
219+
Map<String, Object> params = new LinkedHashMap<>();
207220
Assert.assertTrue(args.length % 2 == 0, "You MUST pass in an even number of arguments");
208221
for (int ix = 0; ix < args.length; ix = ix + 2) {
209222
Object key = args[ix];
@@ -214,6 +227,7 @@ protected Map<String, Object> mkMessageParams(Object validatedValue, ValidationE
214227
return params;
215228
}
216229

230+
217231
private Object mkFieldOrArgPath(ValidationEnvironment validationEnvironment) {
218232
ExecutionPath executionPath = validationEnvironment.getExecutionPath();
219233
ExecutionPath fieldOrArgumentPath = validationEnvironment.getFieldOrArgumentPath();

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import graphql.schema.GraphQLList;
1616
import graphql.schema.GraphQLTypeUtil;
1717
import graphql.util.FpKit;
18+
import graphql.validation.constraints.standard.ArgumentsConstraint;
1819
import graphql.validation.constraints.standard.AssertFalseConstraint;
1920
import graphql.validation.constraints.standard.AssertTrueConstraint;
2021
import graphql.validation.constraints.standard.DecimalMaxConstraint;
@@ -56,6 +57,7 @@ public class DirectiveConstraints implements ValidationRule {
5657
* These are the standard directive rules that come with the system
5758
*/
5859
public final static List<DirectiveConstraint> STANDARD_CONSTRAINTS = Arrays.asList(
60+
new ArgumentsConstraint(),
5961
new AssertFalseConstraint(),
6062
new AssertTrueConstraint(),
6163
new DecimalMaxConstraint(),
Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
package graphql.validation.constraints.standard;
22

33
import graphql.GraphQLError;
4+
import graphql.schema.GraphQLDirective;
45
import graphql.schema.GraphQLFieldDefinition;
56
import graphql.schema.GraphQLFieldsContainer;
67
import graphql.schema.GraphQLInputType;
78
import graphql.validation.constraints.AbstractDirectiveConstraint;
89
import graphql.validation.constraints.Documentation;
10+
import graphql.validation.el.ELSupport;
911
import graphql.validation.rules.ValidationEnvironment;
1012

11-
import javax.el.ELContext;
12-
import javax.el.ExpressionFactory;
13-
import javax.el.ValueExpression;
1413
import java.util.Collections;
1514
import java.util.List;
15+
import java.util.Map;
1616

1717
public class ArgumentsConstraint extends AbstractDirectiveConstraint {
1818

1919
public ArgumentsConstraint() {
20-
super("@Arguments");
20+
super("Arguments");
2121
}
2222

2323

@@ -26,14 +26,14 @@ public Documentation getDocumentation() {
2626
return Documentation.newDocumentation()
2727
.messageTemplate(getMessageTemplate())
2828

29-
.description("TODO")
29+
.description("The provided expression must evaluate to true.")
3030

3131
.example("drivers( first : Int, after : String!, last : Int, before : String) \n" +
32-
" : DriverConnection @Arguments(expr : \"${(! empty first && ! empty after) || (! empty last && ! empty before)}\"")
32+
" : DriverConnection @Arguments(expression : \"${args.containsOneOf('first','last') }\"")
3333

3434
.applicableTypeNames("Output Fields")
3535

36-
.directiveSDL("directive @Arguments(expr : String!, message : String = \"%s\") " +
36+
.directiveSDL("directive @Arguments(expression : String!, message : String = \"%s\") " +
3737
"on FIELD_DEFINITION",
3838
getMessageTemplate())
3939
.build();
@@ -53,16 +53,32 @@ public boolean appliesTo(GraphQLFieldDefinition fieldDefinition, GraphQLFieldsCo
5353
public List<GraphQLError> runValidation(ValidationEnvironment validationEnvironment) {
5454

5555
GraphQLFieldDefinition fieldDefinition = validationEnvironment.getFieldDefinition();
56+
GraphQLDirective directive = validationEnvironment.getContextObject(GraphQLDirective.class);
57+
String expression = curlyBraces(getStrArg(directive, "expression"));
5658

57-
// TODO
59+
Map<String, Object> variables = mkMap(
60+
"fieldDefinition", fieldDefinition,
61+
"args", validationEnvironment.getArgumentValues()
62+
);
63+
ELSupport elSupport = new ELSupport(validationEnvironment.getLocale());
64+
boolean isOK = elSupport.evaluateBoolean(expression, variables);
65+
66+
if (!isOK) {
67+
return mkError(validationEnvironment, directive, mkMessageParams(null, validationEnvironment,
68+
"expression", expression));
69+
70+
}
5871
return Collections.emptyList();
5972
}
6073

61-
private void bindVariable(ExpressionFactory expressionFactory, ELContext elContext, String variableName, Object variable, Class variableClass) {
62-
ValueExpression valueExpression = expressionFactory.createValueExpression(
63-
variable,
64-
variableClass
65-
);
66-
elContext.getVariableMapper().setVariable(variableName, valueExpression);
74+
private String curlyBraces(String expression) {
75+
expression = expression.trim();
76+
if (!expression.startsWith("${") && !expression.startsWith("#{")) {
77+
expression = "${" + expression;
78+
}
79+
if (!expression.startsWith("}")) {
80+
expression = expression + "}";
81+
}
82+
return expression;
6783
}
6884
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package graphql.validation.el;
2+
3+
import graphql.Internal;
4+
5+
import javax.el.ELContext;
6+
import javax.el.MapELResolver;
7+
import java.util.Arrays;
8+
import java.util.List;
9+
import java.util.Map;
10+
11+
@Internal
12+
public class BetterMapELResolver extends MapELResolver {
13+
public static boolean containsOneOf(Map map, List<Object> keys) {
14+
int count = 0;
15+
for (Object key : keys) {
16+
if (map.get(key) != null) {
17+
count++;
18+
}
19+
}
20+
return count == 1;
21+
}
22+
23+
public static boolean containsAllOf(Map map, List<Object> keys) {
24+
for (Object key : keys) {
25+
if (map.get(key) == null) {
26+
return false;
27+
}
28+
}
29+
return true;
30+
}
31+
32+
@Override
33+
public Object invoke(ELContext context, Object base, Object method, Class<?>[] paramTypes, Object[] params) {
34+
35+
if (context == null) {
36+
throw new NullPointerException();
37+
}
38+
39+
if (base instanceof Map) {
40+
context.setPropertyResolved(true);
41+
Map map = (Map) base;
42+
if ("containsOneOf" .equals(method)) {
43+
return containsOneOf(map, Arrays.asList(params));
44+
}
45+
if ("containsAllOf" .equals(method)) {
46+
return containsAllOf(map, Arrays.asList(params));
47+
}
48+
}
49+
return null;
50+
}
51+
52+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package graphql.validation.el;
2+
3+
import graphql.Internal;
4+
import org.hibernate.validator.internal.engine.messageinterpolation.FormatterWrapper;
5+
6+
import javax.el.ELContext;
7+
import javax.el.ELManager;
8+
import javax.el.ExpressionFactory;
9+
import javax.el.StandardELContext;
10+
import javax.el.ValueExpression;
11+
import java.lang.reflect.Method;
12+
import java.util.Locale;
13+
import java.util.Map;
14+
15+
@Internal
16+
@SuppressWarnings("unused")
17+
public class ELSupport {
18+
private static final ExpressionFactory expressionFactory = loadExpressionSupport();
19+
private final StandardELContext elContext;
20+
21+
public ELSupport(Locale locale) {
22+
elContext = new StandardELContext(expressionFactory);
23+
elContext.setLocale(locale);
24+
elContext.addELResolver(new BetterMapELResolver());
25+
// put in standard functions and variables here
26+
bindVariable(elContext, "formatter", new FormatterWrapper(locale));
27+
}
28+
29+
30+
private static ExpressionFactory loadExpressionSupport() {
31+
//
32+
// Do we need fancy class loading support. The Hibernate Validator code jumps though incredible
33+
// class loader tricks so should we? Do they know something we don't?
34+
//
35+
// For now lets keep it simple
36+
//
37+
return ELManager.getExpressionFactory();
38+
}
39+
40+
private void bindMethod(String bindName, String methodName, Class<?>... args) {
41+
elContext.getFunctionMapper().mapFunction("", bindName, loadMethod(methodName, args));
42+
}
43+
44+
private Method loadMethod(String name, Class<?>... args) {
45+
try {
46+
return ELSupport.class.getMethod(name, args);
47+
} catch (NoSuchMethodException e) {
48+
throw new IllegalStateException(e);
49+
}
50+
}
51+
52+
private ValueExpression bindVariable(ELContext elContext, String variableName, Object variableValue) {
53+
ValueExpression valueExpression = expressionFactory.createValueExpression(
54+
variableValue,
55+
Object.class
56+
);
57+
return elContext.getVariableMapper().setVariable(variableName, valueExpression);
58+
}
59+
60+
public boolean evaluateBoolean(String expression, Map<String, Object> variables) {
61+
return evaluateImpl(expression, variables, Boolean.class);
62+
63+
}
64+
65+
public Object evaluate(String expression, Map<String, Object> variables) {
66+
return evaluateImpl(expression, variables, Object.class);
67+
}
68+
69+
@SuppressWarnings("unchecked")
70+
private <T> T evaluateImpl(String expression, Map<String, Object> variables, Class<T> expectedResultClass) {
71+
StandardELContext context = new StandardELContext(elContext);
72+
for (Map.Entry<String, Object> entry : variables.entrySet()) {
73+
bindVariable(context, entry.getKey(), entry.getValue());
74+
}
75+
ValueExpression result = expressionFactory.createValueExpression(context, expression, expectedResultClass);
76+
return (T) result.getValue(context);
77+
}
78+
79+
80+
}

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
import java.util.Map;
1717
import java.util.function.Consumer;
1818

19-
import static graphql.Assert.assertNotNull;
20-
2119
@PublicApi
2220
public class ValidationEnvironment {
2321

@@ -111,7 +109,7 @@ public ValidationEnvironment transform(Consumer<Builder> builderConsumer) {
111109
public static class Builder {
112110
private final Map<Class, Object> contextMap = new HashMap<>();
113111
private GraphQLArgument argument;
114-
private Map<String, Object> argumentValues;
112+
private Map<String, Object> argumentValues = new HashMap<>();
115113
private ExecutionPath executionPath;
116114
private GraphQLFieldDefinition fieldDefinition;
117115
private ExecutionPath fieldOrArgumentPath = ExecutionPath.rootPath();
@@ -210,9 +208,6 @@ public Builder locale(Locale locale) {
210208
}
211209

212210
public ValidationEnvironment build() {
213-
assertNotNull(argument);
214-
assertNotNull(fieldOrArgumentType);
215-
assertNotNull(fieldOrArgumentPath);
216211
return new ValidationEnvironment(this);
217212
}
218213
}

src/main/resources/graphql/validation/ValidationMessages.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#
22
# This contains the library provided validation messages
33
#
4+
graphql.validation.Arguments.message=the expression must evaluate to true
45
graphql.validation.AssertFalse.message=must be false
56
graphql.validation.AssertTrue.message=must be true
67
graphql.validation.DecimalMax.message=must be less than ${inclusive == true ? 'or equal to ' : ''}{value}
@@ -17,3 +18,4 @@ graphql.validation.Positive.message=must be greater than 0
1718
graphql.validation.PositiveOrZero.message=must be greater than or equal to 0
1819
graphql.validation.Range.message=range must be between {min} and {max}
1920
graphql.validation.Size.message=size must be between {min} and {max}
21+

0 commit comments

Comments
 (0)