Skip to content

Commit 08e6f7d

Browse files
committed
Add validation function caching
1 parent cc071ee commit 08e6f7d

File tree

10 files changed

+207
-78
lines changed

10 files changed

+207
-78
lines changed

src/main/java/com/relogiclabs/json/schema/JsonAssert.java

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,72 +2,83 @@
22

33
import com.relogiclabs.json.schema.internal.util.DebugUtilities;
44
import com.relogiclabs.json.schema.message.MessageFormatter;
5+
import com.relogiclabs.json.schema.tree.DataTree;
56
import com.relogiclabs.json.schema.tree.JsonTree;
67
import com.relogiclabs.json.schema.tree.RuntimeContext;
78
import com.relogiclabs.json.schema.tree.SchemaTree;
9+
import com.relogiclabs.json.schema.tree.TreeType;
810
import lombok.Getter;
911

12+
import static com.relogiclabs.json.schema.tree.TreeType.JSON_TREE;
13+
import static com.relogiclabs.json.schema.tree.TreeType.SCHEMA_TREE;
14+
1015
/**
1116
* The class provides assertion functionalities to validate JSON documents against
12-
* a specific JSON Schema.
17+
* a Schema or JSON.
1318
*/
1419
@Getter
1520
public class JsonAssert {
1621
private final RuntimeContext runtime;
17-
private final SchemaTree schemaTree;
22+
private final DataTree expectedTree;
1823

1924
/**
2025
* Initializes a new instance of the {@link JsonAssert} class for the
2126
* specified Schema string.
2227
* @param schema A Schema string for validation or conformation
2328
*/
2429
public JsonAssert(String schema) {
25-
runtime = new RuntimeContext(MessageFormatter.SCHEMA_ASSERTION, true);
26-
schemaTree = new SchemaTree(runtime, schema);
30+
this(schema, SCHEMA_TREE);
2731
}
2832

2933
/**
30-
* Tests whether the input JSON string conforms to the Schema specified
31-
* in the {@link JsonAssert} constructor.
32-
* @param jsonActual The actual JSON to conform or validate
34+
* Initializes a new instance of the {@link JsonAssert} class for the specified
35+
* {@code expected} string, which can be either a Schema or a JSON representation.
36+
* @param expected An expected Schema or JSON string for validation or conformation
37+
* @param type The type of string provided by {@code expected}, indicating whether it represents
38+
* a Schema or JSON. Use {@link TreeType#SCHEMA_TREE} for Schema and {@link TreeType#JSON_TREE}
39+
* for JSON.
3340
*/
34-
public void isValid(String jsonActual) {
35-
runtime.getExceptions().clear();
36-
var jsonTree = new JsonTree(runtime, jsonActual);
37-
DebugUtilities.print(schemaTree, jsonTree);
38-
schemaTree.getRoot().match(jsonTree.getRoot());
39-
if(!schemaTree.getRoot().match(jsonTree.getRoot()))
41+
public JsonAssert(String expected, TreeType type) {
42+
if(type == SCHEMA_TREE) {
43+
runtime = new RuntimeContext(MessageFormatter.SCHEMA_ASSERTION, true);
44+
expectedTree = new SchemaTree(runtime, expected);
45+
} else {
46+
runtime = new RuntimeContext(MessageFormatter.JSON_ASSERTION, true);
47+
expectedTree = new JsonTree(runtime, expected);
48+
}
49+
}
50+
51+
/**
52+
* Tests whether the input JSON string conforms to the expected Schema or JSON
53+
* specified in the {@link JsonAssert} constructor.
54+
* @param json The actual input JSON to conform or validate
55+
*/
56+
public void isValid(String json) {
57+
runtime.clear();
58+
var jsonTree = new JsonTree(runtime, json);
59+
DebugUtilities.print(expectedTree, jsonTree);
60+
if(!expectedTree.match(jsonTree))
4061
throw new IllegalStateException("Exception not thrown");
4162
}
4263

4364
/**
4465
* Tests whether the specified JSON string conforms to the given Schema string
4566
* and throws an exception if the JSON string does not conform to the Schema.
46-
* @param schemaExpected The expected Schema to conform or validate
47-
* @param jsonActual The actual JSON to conform or validate
67+
* @param schema The expected Schema to conform or validate
68+
* @param json The actual JSON to conform or validate
4869
*/
49-
public static void isValid(String schemaExpected, String jsonActual) {
50-
var runtime = new RuntimeContext(MessageFormatter.SCHEMA_ASSERTION, true);
51-
var schemaTree = new SchemaTree(runtime, schemaExpected);
52-
var jsonTree = new JsonTree(runtime, jsonActual);
53-
DebugUtilities.print(schemaTree, jsonTree);
54-
if(!schemaTree.getRoot().match(jsonTree.getRoot()))
55-
throw new IllegalStateException("Exception not thrown");
70+
public static void isValid(String schema, String json) {
71+
new JsonAssert(schema).isValid(json);
5672
}
5773

5874
/**
5975
* Tests if the provided JSON strings are logically equivalent, meaning their structural
6076
* composition and internal data are identical. If the JSON strings are not equivalent,
6177
* an exception is thrown.
62-
* @param jsonExpected The expected JSON to compare
63-
* @param jsonActual The actual JSON to compare
78+
* @param expected The expected JSON to compare
79+
* @param actual The actual JSON to compare
6480
*/
65-
public static void areEqual(String jsonExpected, String jsonActual) {
66-
var runtime = new RuntimeContext(MessageFormatter.JSON_ASSERTION, true);
67-
var expectedTree = new JsonTree(runtime, jsonExpected);
68-
var actualTree = new JsonTree(runtime, jsonActual);
69-
DebugUtilities.print(expectedTree, actualTree);
70-
if(!expectedTree.getRoot().match(actualTree.getRoot()))
71-
throw new IllegalStateException("Exception not thrown");
81+
public static void areEqual(String expected, String actual) {
82+
new JsonAssert(expected, JSON_TREE).isValid(actual);
7283
}
7384
}

src/main/java/com/relogiclabs/json/schema/JsonSchema.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
11
package com.relogiclabs.json.schema;
22

3+
import com.relogiclabs.json.schema.internal.tree.ExceptionRegistry;
34
import com.relogiclabs.json.schema.internal.util.DebugUtilities;
45
import com.relogiclabs.json.schema.message.MessageFormatter;
56
import com.relogiclabs.json.schema.tree.JsonTree;
67
import com.relogiclabs.json.schema.tree.RuntimeContext;
78
import com.relogiclabs.json.schema.tree.SchemaTree;
89
import lombok.Getter;
910

10-
import java.util.Queue;
11-
1211
/**
1312
* {@code JsonSchema} provides Schema validation functionalities for JSON document.
1413
*/
1514
@Getter
1615
public class JsonSchema {
1716
private final RuntimeContext runtime;
1817
private final SchemaTree schemaTree;
19-
private final Queue<Exception> exceptions;
18+
private final ExceptionRegistry exceptions;
2019

2120
/**
2221
* Initializes a new instance of the {@link JsonSchema} class for the
@@ -36,10 +35,10 @@ public JsonSchema(String schema) {
3635
* @return Returns {@code true} if the JSON string conforms to the Schema and {@code false} otherwise.
3736
*/
3837
public boolean isValid(String json) {
39-
exceptions.clear();
38+
runtime.clear();
4039
var jsonTree = new JsonTree(runtime, json);
4140
DebugUtilities.print(schemaTree, jsonTree);
42-
return schemaTree.getRoot().match(jsonTree.getRoot());
41+
return schemaTree.match(jsonTree);
4342
}
4443

4544
/**
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.relogiclabs.json.schema.internal.tree;
2+
3+
import lombok.Getter;
4+
import lombok.Setter;
5+
6+
import java.util.Iterator;
7+
import java.util.LinkedList;
8+
import java.util.Queue;
9+
import java.util.function.Supplier;
10+
11+
public class ExceptionRegistry implements Iterable<Exception> {
12+
private int disableException;
13+
14+
@Getter private final Queue<Exception> exceptions;
15+
@Getter @Setter private boolean throwException;
16+
@Getter @Setter private int cutoffLimit = 200;
17+
18+
public ExceptionRegistry(boolean throwException) {
19+
this.throwException = throwException;
20+
this.exceptions = new LinkedList<>();
21+
}
22+
23+
public void tryAdd(Exception exception) {
24+
if(disableException == 0 && exceptions.size() < cutoffLimit)
25+
exceptions.add(exception);
26+
}
27+
28+
public void tryThrow(RuntimeException exception) {
29+
if(throwException && disableException == 0) throw exception;
30+
}
31+
32+
public <T> T tryExecute(Supplier<T> function) {
33+
try {
34+
disableException += 1;
35+
return function.get();
36+
} finally {
37+
disableException -= 1;
38+
}
39+
}
40+
41+
@Override
42+
public Iterator<Exception> iterator() {
43+
return exceptions.iterator();
44+
}
45+
46+
public void clear() {
47+
exceptions.clear();
48+
}
49+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.relogiclabs.json.schema.internal.tree;
2+
3+
import com.relogiclabs.json.schema.types.JFunction;
4+
import com.relogiclabs.json.schema.types.JNode;
5+
import lombok.Getter;
6+
import lombok.Setter;
7+
import lombok.Value;
8+
9+
import java.util.ArrayList;
10+
import java.util.Iterator;
11+
import java.util.List;
12+
13+
public class FunctionCache implements Iterable<FunctionCache.Entry> {
14+
15+
@Value
16+
public static class Entry {
17+
MethodPointer methodPointer;
18+
Object[] arguments;
19+
20+
public boolean isTargetMatch(JNode target) {
21+
return methodPointer.getParameter(0).getType().isInstance(target.getDerived());
22+
}
23+
24+
public Object invoke(JFunction function, JNode target) {
25+
arguments[0] = target.getDerived();
26+
return methodPointer.invoke(function, arguments);
27+
}
28+
}
29+
30+
@Getter @Setter
31+
private static int sizeLimit = 10;
32+
private final List<Entry> cache;
33+
34+
public FunctionCache() {
35+
this.cache = new ArrayList<>(sizeLimit);
36+
}
37+
38+
public void add(MethodPointer methodPointer, Object[] arguments) {
39+
if(cache.size() > sizeLimit) cache.remove(0);
40+
arguments[0] = null;
41+
cache.add(new Entry(methodPointer, arguments));
42+
}
43+
44+
@Override
45+
public Iterator<Entry> iterator() {
46+
return cache.iterator();
47+
}
48+
}

src/main/java/com/relogiclabs/json/schema/internal/tree/FunctionManager.java renamed to src/main/java/com/relogiclabs/json/schema/internal/tree/FunctionRegistry.java

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.relogiclabs.json.schema.exception.JsonSchemaException;
1010
import com.relogiclabs.json.schema.exception.NotFoundClassException;
1111
import com.relogiclabs.json.schema.function.FunctionBase;
12+
import com.relogiclabs.json.schema.function.FutureValidator;
1213
import com.relogiclabs.json.schema.message.ActualDetail;
1314
import com.relogiclabs.json.schema.message.ErrorDetail;
1415
import com.relogiclabs.json.schema.message.ExpectedDetail;
@@ -45,12 +46,12 @@
4546
import static com.relogiclabs.json.schema.message.ErrorCode.FUNC04;
4647
import static com.relogiclabs.json.schema.message.ErrorCode.FUNC05;
4748

48-
public final class FunctionManager {
49+
public final class FunctionRegistry {
4950
private final Set<String> includes;
5051
private final Map<FunctionKey, List<MethodPointer>> functions;
5152
private final RuntimeContext runtime;
5253

53-
public FunctionManager(RuntimeContext runtime) {
54+
public FunctionRegistry(RuntimeContext runtime) {
5455
this.runtime = runtime;
5556
this.includes = new HashSet<>();
5657
this.functions = new HashMap<>();
@@ -88,11 +89,10 @@ private Map<FunctionKey, List<MethodPointer>> extractMethods(Class<?> subclass,
8889
if(!baseclass.isAssignableFrom(m.getDeclaringClass())) continue;
8990
if(baseclass == m.getDeclaringClass()) continue;
9091
Parameter[] parameters = m.getParameters();
91-
if(m.getReturnType() != boolean.class && m.getReturnType() != Boolean.class)
92-
throw new InvalidFunctionException(FUNC01, concat("Function [", getSignature(m),
93-
"] requires return type boolean"));
92+
if(!isValidReturnType(m.getReturnType())) throw new InvalidFunctionException(FUNC01,
93+
concat("Function [", getSignature(m), "] requires valid return type"));
9494
if(parameters.length < 1) throw new InvalidFunctionException(FUNC02,
95-
concat("Function [", getSignature(m), "] requires minimum one parameter"));
95+
concat("Function [", getSignature(m), "] requires target parameter"));
9696
var key = new FunctionKey(m, getParameterCount(parameters));
9797
var value = new MethodPointer(instance, m, parameters);
9898
var valueList = functions.get(key);
@@ -103,6 +103,13 @@ private Map<FunctionKey, List<MethodPointer>> extractMethods(Class<?> subclass,
103103
return functions;
104104
}
105105

106+
private boolean isValidReturnType(Class<?> type) {
107+
if(type == boolean.class) return true;
108+
if(type == Boolean.class) return true;
109+
if(type == FutureValidator.class) return true;
110+
return false;
111+
}
112+
106113
private int getParameterCount(Parameter[] parameters) {
107114
for(var p : parameters) if(p.isVarArgs()) return -1;
108115
return parameters.length;
@@ -128,7 +135,17 @@ private static CommonException createException(String code, Exception ex, Class<
128135
code, "Fail to create instance of " + type.getName(), context), ex);
129136
}
130137

138+
private boolean handleValidator(Object result) {
139+
return result instanceof FutureValidator validator
140+
? runtime.addValidator(validator)
141+
: (boolean) result;
142+
}
143+
131144
public boolean invokeFunction(JFunction function, JNode target) {
145+
for(var e : function.getCache()) {
146+
if (e.isTargetMatch(target))
147+
return handleValidator(e.invoke(function, target));
148+
}
132149
var methods = getMethods(function);
133150
Parameter mismatchParameter = null;
134151

@@ -137,11 +154,14 @@ public boolean invokeFunction(JFunction function, JNode target) {
137154
var arguments = function.getArguments();
138155
var schemaArgs = processArgs(parameters, arguments);
139156
if(schemaArgs == null) continue;
140-
if(isMatch(parameters.get(0), target))
141-
return method.invoke(function, addTarget(schemaArgs, target));
157+
if(isMatch(parameters.get(0), target)) {
158+
Object[] allArgs = addTarget(schemaArgs, target).toArray();
159+
var result = method.invoke(function, allArgs);
160+
function.getCache().add(method, allArgs);
161+
return handleValidator(result);
162+
}
142163
mismatchParameter = parameters.get(0);
143164
}
144-
145165
if(mismatchParameter != null)
146166
return failWith(new JsonSchemaException(new ErrorDetail(FUNC03,
147167
"Function ", function.getOutline(), " is incompatible with the target data type"),
@@ -164,10 +184,8 @@ private List<MethodPointer> getMethods(JFunction function) {
164184
}
165185

166186
private static List<Object> addTarget(List<Object> arguments, JNode target) {
167-
var args = new ArrayList<>(1 + arguments.size());
168-
args.add(target);
169-
args.addAll(arguments);
170-
return args;
187+
arguments.add(0, target.getDerived());
188+
return arguments;
171189
}
172190

173191
private static List<Object> processArgs(List<Parameter> parameters, List<JNode> arguments) {
@@ -187,7 +205,7 @@ private static List<Object> processArgs(List<Parameter> parameters, List<JNode>
187205
}
188206

189207
private static boolean isMatch(Parameter parameter, JNode argument) {
190-
return parameter.getType().isInstance(argument);
208+
return parameter.getType().isInstance(argument.getDerived());
191209
}
192210

193211
private static Object processVarArgs(Parameter parameter, List<JNode> arguments) {

src/main/java/com/relogiclabs/json/schema/internal/tree/MethodPointer.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,16 @@ public MethodPointer(FunctionBase instance, Method method, Parameter[] parameter
2929
this.parameters = List.of(parameters);
3030
}
3131

32-
public boolean invoke(JFunction function, List<Object> arguments) {
32+
public Parameter getParameter(int index) {
33+
return parameters.get(index);
34+
}
35+
36+
public Object invoke(JFunction function, Object[] arguments) {
3337
try {
3438
instance.setFunction(function);
35-
Object result = method.invoke(instance, arguments.toArray());
36-
if(!(result instanceof Boolean boolResult)) throw new IllegalStateException();
37-
return boolResult;
39+
Object result = method.invoke(instance, arguments);
40+
if(result == null) throw new IllegalStateException("Function return cannot be null");
41+
return result;
3842
} catch (InvocationTargetException e) {
3943
throw new TargetInvocationException(FUNC07,
4044
"Target invocation exception occurred", e.getCause());

0 commit comments

Comments
 (0)