Skip to content

Commit 67fcbd6

Browse files
committed
Add validation function caching
1 parent 0f34bc0 commit 67fcbd6

File tree

12 files changed

+180
-69
lines changed

12 files changed

+180
-69
lines changed

JsonSchema/JsonSchema.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
<AssemblyName>RelogicLabs.JsonSchema</AssemblyName>
99
<Authors>Relogic Labs</Authors>
1010
<Company>Relogic Labs</Company>
11-
<Version>1.8.0</Version>
12-
<PackageVersion>1.8.0</PackageVersion>
13-
<AssemblyVersion>1.8.0</AssemblyVersion>
11+
<Version>1.9.0</Version>
12+
<PackageVersion>1.9.0</PackageVersion>
13+
<AssemblyVersion>1.9.0</AssemblyVersion>
1414
<PackageTags>JsonSchema;Schema;Json;Validation;Assert;Test</PackageTags>
1515
<Copyright>Copyright © Relogic Labs. All rights reserved.</Copyright>
1616
<NeutralLanguage>en</NeutralLanguage>
Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,70 @@
11
using RelogicLabs.JsonSchema.Message;
22
using RelogicLabs.JsonSchema.Tree;
33
using RelogicLabs.JsonSchema.Utilities;
4+
using static RelogicLabs.JsonSchema.Tree.TreeType;
45

56
namespace RelogicLabs.JsonSchema;
67

78
/// <summary>
8-
/// Provides assertion functionalities to validate JSON document against JSON Schema.
9+
/// Provides assertion functionalities to validate Json document against a Schema or Json.
910
/// </summary>
1011
public class JsonAssert
1112
{
1213
public RuntimeContext Runtime { get; }
13-
public SchemaTree SchemaTree { get; }
14-
14+
public IDataTree DataTree { get; }
15+
1516
/// <summary>
1617
/// Initializes a new instance of the <see cref="JsonAssert"/> class for the
1718
/// specified Schema string.
1819
/// </summary>
1920
/// <param name="schema">A Schema string for validation or conformation.</param>
20-
public JsonAssert(string schema)
21+
public JsonAssert(string schema) : this(schema, SCHEMA_TREE) { }
22+
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="JsonAssert"/> class for the
25+
/// specified <paramref name="expected"/> string which can be either a Schema or a Json
26+
/// representation.
27+
/// </summary>
28+
/// <param name="expected">An expected Schema or Json string for validation or conformation.</param>
29+
/// <param name="type">The type of string provided by <paramref name="expected"/>, indicating
30+
/// whether it represents a Schema or Json. Use <see cref="TreeType.SCHEMA_TREE"/> for Schema
31+
/// and <see cref="TreeType.JSON_TREE"/> for Json.</param>
32+
public JsonAssert(string expected, TreeType type)
2133
{
22-
Runtime = new(MessageFormatter.SchemaAssertion, true);
23-
SchemaTree = new(Runtime, schema);
34+
if(type == SCHEMA_TREE)
35+
{
36+
Runtime = new RuntimeContext(MessageFormatter.SchemaAssertion, true);
37+
DataTree = new SchemaTree(Runtime, expected);
38+
}
39+
else
40+
{
41+
Runtime = new RuntimeContext(MessageFormatter.JsonAssertion, true);
42+
DataTree = new JsonTree(Runtime, expected);
43+
}
2444
}
25-
45+
2646
/// <summary>
2747
/// Tests whether the input JSON string conforms to the Schema specified
2848
/// in the <see cref="JsonAssert"/> constructor.
2949
/// </summary>
30-
/// <param name="json">The actual JSON to conform or validate.</param>
31-
public void IsValid(string json)
50+
/// <param name="jsonActual">The actual JSON to conform or validate.</param>
51+
public void IsValid(string jsonActual)
3252
{
33-
Runtime.Exceptions.Clear();
34-
JsonTree jsonTree = new(Runtime, json);
35-
DebugUtilities.Print(SchemaTree, jsonTree);
36-
if(!SchemaTree.Root.Match(jsonTree.Root))
37-
throw new InvalidOperationException("Exception not thrown");
53+
Runtime.Clear();
54+
JsonTree jsonTree = new(Runtime, jsonActual);
55+
DebugUtilities.Print(DataTree, jsonTree);
56+
if(!DataTree.Match(jsonTree))
57+
throw new InvalidOperationException("Invalid runtime state");
3858
}
39-
59+
4060
/// <summary>
4161
/// Tests whether the specified JSON string conforms to the given Schema string
4262
/// and throws an exception if the JSON string does not conform to the Schema.
4363
/// </summary>
4464
/// <param name="schemaExpected">The expected Schema to conform or validate.</param>
4565
/// <param name="jsonActual">The actual JSON to conform or validate.</param>
4666
public static void IsValid(string schemaExpected, string jsonActual)
47-
{
48-
RuntimeContext runtime = new(MessageFormatter.SchemaAssertion, true);
49-
SchemaTree schemaTree = new(runtime, schemaExpected);
50-
JsonTree jsonTree = new(runtime, jsonActual);
51-
DebugUtilities.Print(schemaTree, jsonTree);
52-
if(!schemaTree.Root.Match(jsonTree.Root))
53-
throw new InvalidOperationException("Exception not thrown");
54-
}
67+
=> new JsonAssert(schemaExpected).IsValid(jsonActual);
5568

5669
/// <summary>
5770
/// Tests if the provided JSON strings are logically equivalent, meaning their structural
@@ -61,12 +74,5 @@ public static void IsValid(string schemaExpected, string jsonActual)
6174
/// <param name="jsonExpected">The expected JSON to compare.</param>
6275
/// <param name="jsonActual">The actual JSON to compare.</param>
6376
public static void AreEqual(string jsonExpected, string jsonActual)
64-
{
65-
RuntimeContext runtime = new(MessageFormatter.JsonAssertion, true);
66-
JsonTree expectedTree = new(runtime, jsonExpected);
67-
JsonTree actualTree = new(runtime, jsonActual);
68-
DebugUtilities.Print(expectedTree, actualTree);
69-
if(!expectedTree.Root.Match(actualTree.Root))
70-
throw new InvalidOperationException("Exception not thrown");
71-
}
77+
=> new JsonAssert(jsonExpected, JSON_TREE).IsValid(jsonActual);
7278
}

JsonSchema/RelogicLabs/JsonSchema/JsonSchema.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ public class JsonSchema
1111
{
1212
public RuntimeContext Runtime { get; }
1313
public SchemaTree SchemaTree { get; }
14-
public Queue<Exception> Exceptions { get; }
15-
14+
public ExceptionRegistry Exceptions { get; }
15+
1616
/// <summary>
1717
/// Initializes a new instance of the <see cref="JsonSchema"/> class for the
1818
/// specified Schema string.
@@ -34,10 +34,10 @@ public JsonSchema(string schema)
3434
/// <c>false</c>.</returns>
3535
public bool IsValid(string json)
3636
{
37-
Exceptions.Clear();
37+
Runtime.Clear();
3838
JsonTree jsonTree = new(Runtime, json);
3939
DebugUtilities.Print(SchemaTree, jsonTree);
40-
var result = SchemaTree.Root.Match(jsonTree.Root);
40+
var result = SchemaTree.Match(jsonTree);
4141
return result;
4242
}
4343

@@ -50,7 +50,7 @@ public void WriteError()
5050
foreach(var exception in Exceptions)
5151
Console.Error.WriteLine(exception.Message);
5252
}
53-
53+
5454
/// <summary>
5555
/// Indicates whether the input JSON string conforms to the given Schema string.
5656
/// </summary>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System.Collections;
2+
3+
namespace RelogicLabs.JsonSchema.Tree;
4+
5+
public class ExceptionRegistry : IEnumerable<Exception>
6+
{
7+
private int _disableException;
8+
9+
public bool ThrowException { get; set; }
10+
public int CutoffLimit { get; set; } = 200;
11+
public Queue<Exception> Exceptions { get; }
12+
13+
14+
internal ExceptionRegistry(bool throwException)
15+
{
16+
ThrowException = throwException;
17+
Exceptions = new Queue<Exception>();
18+
}
19+
20+
public void TryAdd(Exception exception)
21+
{
22+
if(_disableException == 0 && Exceptions.Count < CutoffLimit)
23+
Exceptions.Enqueue(exception);
24+
}
25+
26+
public void TryThrow(Exception exception)
27+
{
28+
if(ThrowException && _disableException == 0) throw exception;
29+
}
30+
31+
internal T TryExecute<T>(Func<T> function)
32+
{
33+
try
34+
{
35+
_disableException += 1;
36+
return function();
37+
}
38+
finally
39+
{
40+
_disableException -= 1;
41+
}
42+
}
43+
44+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
45+
public IEnumerator<Exception> GetEnumerator() => Exceptions.GetEnumerator();
46+
public void Clear() => Exceptions.Clear();
47+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System.Collections;
2+
using RelogicLabs.JsonSchema.Types;
3+
4+
namespace RelogicLabs.JsonSchema.Tree;
5+
6+
internal class FunctionCache : IEnumerable<FunctionCache.Entry>
7+
{
8+
public record Entry(MethodPointer MethodPointer, object?[] Arguments)
9+
{
10+
public bool IsTargetMatch(JNode target) => MethodPointer.Parameters[0]
11+
.ParameterType.IsInstanceOfType(target.Derived);
12+
13+
public object Invoke(JFunction function, JNode target)
14+
{
15+
Arguments[0] = target.Derived;
16+
return MethodPointer.Invoke(function, Arguments);
17+
}
18+
}
19+
20+
public static int SizeLimit { get; set; } = 10;
21+
private List<Entry> _cache;
22+
23+
public FunctionCache() => _cache = new List<Entry>(SizeLimit);
24+
25+
public void Add(MethodPointer methodPointer, object?[] arguments)
26+
{
27+
if(_cache.Count > SizeLimit) _cache.RemoveAt(0);
28+
arguments[0] = null;
29+
_cache.Add(new Entry(methodPointer, arguments));
30+
}
31+
32+
public IEnumerator<Entry> GetEnumerator() => _cache.GetEnumerator();
33+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
34+
}

JsonSchema/RelogicLabs/JsonSchema/Tree/FunctionRegistry.cs

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public sealed class FunctionRegistry
1515
private readonly Dictionary<FunctionKey, List<MethodPointer>> _functions = new();
1616
private readonly RuntimeContext _runtime;
1717

18-
public FunctionRegistry(RuntimeContext runtime) => _runtime = runtime;
18+
internal FunctionRegistry(RuntimeContext runtime) => _runtime = runtime;
1919

2020
public JInclude AddClass(JInclude include)
2121
{
@@ -88,10 +88,11 @@ private Dictionary<FunctionKey, List<MethodPointer>> ExtractMethods(
8888
if(!baseclass.IsAssignableFrom(m.DeclaringType)) continue;
8989
if(baseclass == m.DeclaringType) continue;
9090
ParameterInfo[] parameters = m.GetParameters();
91-
if(m.ReturnType != typeof(bool)) throw new InvalidFunctionException(FUNC01,
92-
$"Function [{m.GetSignature()}] requires return type boolean");
91+
if(!IsValidReturnType(m.ReturnType))
92+
throw new InvalidFunctionException(FUNC01,
93+
$"Function [{m.GetSignature()}] requires valid return type");
9394
if(parameters.Length < 1) throw new InvalidFunctionException(FUNC02,
94-
$"Function [{m.GetSignature()}] requires minimum one parameter");
95+
$"Function [{m.GetSignature()}] requires target parameter");
9596
var key = new FunctionKey(m, GetParameterCount(parameters));
9697
var value = new MethodPointer(instance, m, parameters);
9798
functions.TryGetValue(key, out var valueList);
@@ -102,6 +103,12 @@ private Dictionary<FunctionKey, List<MethodPointer>> ExtractMethods(
102103
return functions;
103104
}
104105

106+
private static bool IsValidReturnType(Type type) {
107+
if(type == typeof(bool)) return true;
108+
if(type == typeof(FutureValidator)) return true;
109+
return false;
110+
}
111+
105112
private static int GetParameterCount(ICollection<ParameterInfo> parameters)
106113
{
107114
foreach(var p in parameters) if(IsParams(p)) return -1;
@@ -114,8 +121,20 @@ private static bool IsMatch(ParameterInfo parameter, JNode argument)
114121
private static bool IsParams(ParameterInfo parameter)
115122
=> parameter.IsDefined(typeof(ParamArrayAttribute), false);
116123

124+
private bool HandleValidator(object result)
125+
{
126+
return result is FutureValidator validator
127+
? _runtime.AddValidator(validator)
128+
: (bool) result;
129+
}
130+
117131
public bool InvokeFunction(JFunction function, JNode target)
118132
{
133+
foreach(var e in function.Cache)
134+
{
135+
if(e.IsTargetMatch(target))
136+
return HandleValidator(e.Invoke(function, target));
137+
}
119138
var methods = GetMethods(function);
120139
ParameterInfo? mismatchParameter = null;
121140

@@ -125,8 +144,13 @@ public bool InvokeFunction(JFunction function, JNode target)
125144
var _arguments = function.Arguments;
126145
var schemaArgs = ProcessArguments(_parameters, _arguments);
127146
if(schemaArgs == null) continue;
128-
if(IsMatch(_parameters[0], target)) return method.Invoke(function,
129-
AddTarget(schemaArgs, target.Derived));
147+
if(IsMatch(_parameters[0], target))
148+
{
149+
object?[] allArgs = AddTarget(schemaArgs, target).ToArray();
150+
var result = method.Invoke(function, allArgs);
151+
function.Cache.Add(method, allArgs);
152+
return HandleValidator(result);
153+
}
130154
mismatchParameter = _parameters[0];
131155
}
132156
if(mismatchParameter != null)
@@ -138,14 +162,13 @@ public bool InvokeFunction(JFunction function, JNode target)
138162
GetTypeName(target.GetType())} of {target}")));
139163

140164
return FailWith(new FunctionNotFoundException(MessageFormatter
141-
.FormatForSchema(FUNC04, function.GetOutline(), function.Context)));
165+
.FormatForSchema(FUNC04, function.GetOutline(), function)));
142166
}
143167

144-
private static List<object> AddTarget(IList<object> arguments, JNode target)
168+
private static List<object> AddTarget(List<object> arguments, JNode target)
145169
{
146-
List<object> _arguments = new(1 + arguments.Count) { target };
147-
_arguments.AddRange(arguments);
148-
return _arguments;
170+
arguments.Insert(0, target.Derived);
171+
return arguments;
149172
}
150173

151174
private List<MethodPointer> GetMethods(JFunction function)
@@ -156,7 +179,7 @@ private List<MethodPointer> GetMethods(JFunction function)
156179
function.Name, -1), out methodPointers);
157180
if(methodPointers == null)
158181
throw new FunctionNotFoundException(MessageFormatter
159-
.FormatForSchema(FUNC05, $"Not found {function.GetOutline()}", function.Context));
182+
.FormatForSchema(FUNC05, $"Not found {function.GetOutline()}", function));
160183
return methodPointers;
161184
}
162185

@@ -193,6 +216,5 @@ private List<MethodPointer> GetMethods(JFunction function)
193216
return _arguments;
194217
}
195218

196-
private bool FailWith(Exception exception)
197-
=> _runtime.FailWith(exception);
219+
private bool FailWith(Exception exception) => _runtime.FailWith(exception);
198220
}

JsonSchema/RelogicLabs/JsonSchema/Tree/JsonTree.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,16 @@ public sealed class JsonTree : IDataTree
1212
public JRoot Root { get; }
1313
public TreeType Type => JSON_TREE;
1414

15-
public JsonTree(RuntimeContext context, string input)
15+
public JsonTree(RuntimeContext runtime, string input)
1616
{
17-
Runtime = context;
17+
Runtime = runtime;
1818
JsonLexer jsonLexer = new(CharStreams.fromString(input));
1919
jsonLexer.RemoveErrorListeners();
2020
jsonLexer.AddErrorListener(LexerErrorListener.Json);
2121
JsonParser jsonParser = new(new CommonTokenStream(jsonLexer));
2222
jsonParser.RemoveErrorListeners();
2323
jsonParser.AddErrorListener(ParserErrorListener.Json);
24-
Root = (JRoot) new JsonTreeVisitor(context).Visit(jsonParser.json());
24+
Root = (JRoot) new JsonTreeVisitor(runtime).Visit(jsonParser.json());
2525
}
2626

2727
public bool Match(IDataTree dataTree)

JsonSchema/RelogicLabs/JsonSchema/Tree/MethodPointer.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ internal MethodPointer(FunctionBase instance, MethodInfo method,
1919
Parameters = parameters.ToReadOnlyList();
2020
}
2121

22-
public bool Invoke(JFunction function, List<object> arguments)
22+
public object Invoke(JFunction function, object?[] arguments)
2323
{
2424
Instance.Function = function;
25-
var result = Method.Invoke(Instance, arguments.ToArray());
26-
if(result is not bool _result) throw new InvalidOperationException();
27-
return _result;
25+
var result = Method.Invoke(Instance, arguments);
26+
if(result is null) throw new InvalidOperationException();
27+
return result;
2828
}
2929
}

0 commit comments

Comments
 (0)