Skip to content

Commit d06c59e

Browse files
committed
Update nested data type validation
1 parent 22d4418 commit d06c59e

File tree

5 files changed

+114
-107
lines changed

5 files changed

+114
-107
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.10.0</Version>
12-
<PackageVersion>1.10.0</PackageVersion>
13-
<AssemblyVersion>1.10.0</AssemblyVersion>
11+
<Version>1.12.0</Version>
12+
<PackageVersion>1.12.0</PackageVersion>
13+
<AssemblyVersion>1.12.0</AssemblyVersion>
1414
<PackageTags>JsonSchema;Schema;Json;Validation;Assert;Test</PackageTags>
1515
<Copyright>Copyright © Relogic Labs. All rights reserved.</Copyright>
1616
<NeutralLanguage>en</NeutralLanguage>

JsonSchema/RelogicLabs/JsonSchema/Message/MatchReport.cs

Lines changed: 0 additions & 25 deletions
This file was deleted.

JsonSchema/RelogicLabs/JsonSchema/Tree/ExceptionRegistry.cs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,30 @@ namespace RelogicLabs.JsonSchema.Tree;
55
public class ExceptionRegistry : IEnumerable<Exception>
66
{
77
private int _disableException;
8+
private readonly List<Exception> _exceptions;
89

10+
internal List<Exception> TryBuffer { get; }
911
public bool ThrowException { get; set; }
10-
public int CutoffLimit { get; set; } = 200;
11-
public Queue<Exception> Exceptions { get; }
12-
12+
public int CutoffLimit { get; set; } = 500;
13+
public int Count => _exceptions.Count;
1314

1415
internal ExceptionRegistry(bool throwException)
1516
{
17+
_exceptions = new List<Exception>();
1618
ThrowException = throwException;
17-
Exceptions = new Queue<Exception>();
19+
TryBuffer = new List<Exception>();
1820
}
1921

20-
public void TryAdd(Exception exception)
21-
{
22-
if(_disableException == 0 && Exceptions.Count < CutoffLimit)
23-
Exceptions.Enqueue(exception);
22+
private bool AddException(List<Exception> list, Exception exception) {
23+
if(list.Count <= CutoffLimit) list.Add(exception);
24+
return false;
2425
}
2526

26-
public void TryThrow(Exception exception)
27+
internal bool FailWith(Exception exception)
2728
{
28-
if(ThrowException && _disableException == 0) throw exception;
29+
if(_disableException > 0) return AddException(TryBuffer, exception);
30+
if(ThrowException) throw exception;
31+
return AddException(_exceptions, exception);
2932
}
3033

3134
internal T TryExecute<T>(Func<T> function)
@@ -37,11 +40,17 @@ internal T TryExecute<T>(Func<T> function)
3740
}
3841
finally
3942
{
40-
_disableException -= 1;
43+
if(_disableException >= 1) _disableException -= 1;
44+
else throw new InvalidOperationException("Invalid runtime state");
4145
}
4246
}
4347

4448
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
45-
public IEnumerator<Exception> GetEnumerator() => Exceptions.GetEnumerator();
46-
public void Clear() => Exceptions.Clear();
49+
public IEnumerator<Exception> GetEnumerator() => _exceptions.GetEnumerator();
50+
51+
public void Clear()
52+
{
53+
_exceptions.Clear();
54+
TryBuffer.Clear();
55+
}
4756
}

JsonSchema/RelogicLabs/JsonSchema/Types/JDataType.cs

Lines changed: 20 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
using RelogicLabs.JsonSchema.Utilities;
55
using static RelogicLabs.JsonSchema.Message.ErrorCode;
66
using static RelogicLabs.JsonSchema.Message.ErrorDetail;
7-
using static RelogicLabs.JsonSchema.Message.MatchReport;
7+
using static RelogicLabs.JsonSchema.Message.MessageFormatter;
8+
using static RelogicLabs.JsonSchema.Types.INestedMode;
89
using static RelogicLabs.JsonSchema.Utilities.CommonUtilities;
910

1011
namespace RelogicLabs.JsonSchema.Types;
1112

1213
public sealed class JDataType : JBranch, INestedMode
1314
{
15+
internal const string DataTypeName = "DataTypeName";
1416
public JsonType JsonType { get; }
1517
public bool Nested { get; }
1618
public JAlias? Alias { get; }
@@ -25,61 +27,27 @@ private JDataType(Builder builder) : base(builder)
2527

2628
public override bool Match(JNode node)
2729
{
28-
if(!Nested) return IsMatchCurrent(node);
29-
if(node is not JComposite composite) return false;
30-
IList<JNode> components = composite.GetComponents();
31-
return components.Select(IsMatchCurrent).AllTrue();
32-
}
33-
34-
private bool IsMatchCurrent(JNode node)
35-
=> MatchCurrent(node, out _) == Success;
36-
37-
private MatchReport MatchCurrent(JNode node, out string error)
38-
{
39-
var result = JsonType.Match(node, out error) ? Success : TypeError;
40-
if(Alias == null || result != Success) return result;
41-
Runtime.Definitions.TryGetValue(Alias, out var validator);
42-
if(validator == null) return AliasError;
43-
result = validator.Match(node) ? Success : ArgumentError;
44-
return result;
45-
}
46-
47-
internal bool MatchForReport(JNode node)
48-
{
49-
if(!Nested) return MatchForReport(node, false);
50-
if(node is not JComposite composite) return FailWith(
51-
new JsonSchemaException(
52-
new ErrorDetail(DTYP03, InvalidNestedDataType),
53-
ExpectedDetail.AsInvalidNestedDataType(this),
54-
ActualDetail.AsInvalidNestedDataType(node)));
55-
IList<JNode> components = composite.GetComponents();
56-
bool result = true;
57-
foreach(var c in components) result &= MatchForReport(c, true);
58-
return result;
59-
}
60-
61-
private bool MatchForReport(JNode node, bool nested)
62-
{
63-
var result = MatchCurrent(node, out var error);
64-
if(ReferenceEquals(result, TypeError)) return FailWith(
65-
new JsonSchemaException(
66-
new ErrorDetail(TypeError.GetCode(nested),
30+
if(!JsonType.Match(node, out var error)) return FailTypeWith(
31+
new JsonSchemaException(new ErrorDetail(Nested ? DTYP06 : DTYP04,
6732
FormatMessage(DataTypeMismatch, error)),
6833
ExpectedDetail.AsDataTypeMismatch(this),
6934
ActualDetail.AsDataTypeMismatch(node)));
70-
if(ReferenceEquals(result, AliasError)) return FailWith(
71-
new DefinitionNotFoundException(
72-
MessageFormatter.FormatForSchema(AliasError.GetCode(nested),
73-
$"No definition found for '{Alias}'", Context)));
74-
if(ReferenceEquals(result, ArgumentError)) return FailWith(
75-
new JsonSchemaException(
76-
new ErrorDetail(ArgumentError.GetCode(nested),
77-
DataTypeArgumentFailed),
78-
ExpectedDetail.AsDataTypeArgumentFailed(this),
79-
ActualDetail.AsDataTypeArgumentFailed(node)));
35+
if(Alias is null) return true;
36+
var validator = Runtime.Definitions.TryGetValue(Alias);
37+
if(validator is null) return FailWith(new DefinitionNotFoundException(
38+
FormatForSchema(Nested ? DEFI04 : DEFI03, $"No definition found for '{Alias}'", this)));
39+
if(!validator.Match(node)) return FailWith(new JsonSchemaException(
40+
new ErrorDetail(Nested ? DTYP07 : DTYP05, DataTypeArgumentFailed),
41+
ExpectedDetail.AsDataTypeArgumentFailed(this),
42+
ActualDetail.AsDataTypeArgumentFailed(node)));
8043
return true;
8144
}
8245

46+
private bool FailTypeWith(JsonSchemaException exception) {
47+
exception.SetAttribute(DataTypeName, ToString(true));
48+
return FailWith(exception);
49+
}
50+
8351
private static string FormatMessage(string main, string optional)
8452
=> string.IsNullOrEmpty(optional) ? main : $"{main} ({optional.Uncapitalize()})";
8553

@@ -93,13 +61,13 @@ public override bool Equals(object? obj)
9361
}
9462

9563
internal bool IsMatchNull() => !Nested && JsonType == JsonType.NULL;
96-
public bool IsApplicable(JNode node) => !Nested || node is JComposite;
64+
internal bool IsApplicable(JNode node) => !Nested || node is JComposite;
9765
public override int GetHashCode() => JsonType.GetHashCode();
9866
public override string ToString() => ToString(false);
9967
public string ToString(bool baseForm)
10068
{
10169
StringBuilder builder = new(JsonType.ToString());
102-
if(Nested && !baseForm) builder.Append(INestedMode.NestedMarker);
70+
if(Nested && !baseForm) builder.Append(NestedMarker);
10371
if(Alias != null && !baseForm) builder.Append($"({Alias})");
10472
return builder.ToString();
10573
}

JsonSchema/RelogicLabs/JsonSchema/Types/JValidator.cs

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
using RelogicLabs.JsonSchema.Utilities;
44
using static RelogicLabs.JsonSchema.Message.ErrorCode;
55
using static RelogicLabs.JsonSchema.Message.ErrorDetail;
6+
using static RelogicLabs.JsonSchema.Types.JDataType;
67
using static RelogicLabs.JsonSchema.Utilities.CommonUtilities;
78

89
namespace RelogicLabs.JsonSchema.Types;
910

1011
public sealed class JValidator : JBranch
1112
{
12-
public const string OptionalMarker = "?";
13+
internal const string OptionalMarker = "?";
14+
private readonly List<Exception> _exceptions;
15+
private List<Exception> TryBuffer => Runtime.Exceptions.TryBuffer;
16+
1317
public JNode? Value { get; }
1418
public IList<JFunction> Functions { get; }
1519
public IList<JDataType> DataTypes { get; }
@@ -18,6 +22,7 @@ public sealed class JValidator : JBranch
1822

1923
private JValidator(Builder builder) : base(builder)
2024
{
25+
_exceptions = new List<Exception>();
2126
Value = builder.Value;
2227
Functions = RequireNonNull(builder.Functions);
2328
DataTypes = RequireNonNull(builder.DataTypes);
@@ -35,36 +40,86 @@ public override bool Match(JNode node)
3540
var value = CastType<IJsonType>(node);
3641
if(value == null) return false;
3742
Runtime.Receivers.Receive(Receivers, node);
38-
if(node is JNull && DataTypes.Select(d => d.IsMatchNull()).AnyTrue())
43+
if(node is JNull && DataTypes.Any(d => d.IsMatchNull()))
3944
return true;
4045
if(Value != null) rValue &= Value.Match(value.Node);
4146
if(!rValue) return FailWith(new JsonSchemaException(
4247
new ErrorDetail(VALD01, ValidationFailed),
43-
ExpectedDetail.AsValueMismatch(Value!),
44-
ActualDetail.AsValueMismatch(node)));
48+
ExpectedDetail.AsGeneralValueMismatch(Value!),
49+
ActualDetail.AsGeneralValueMismatch(node)));
4550
var rDataType = MatchDataType(node);
4651
var fDataType = rDataType && DataTypes.Count != 0;
4752
bool rFunction = Functions.Where(f => f.IsApplicable(node) || !fDataType)
48-
.Select(f => f.Match(node)).ForEachTrue() || Functions.Count == 0;
53+
.ForEachTrue(f => f.Match(node));
4954
return rValue & rDataType & rFunction;
5055
}
5156

5257
private bool MatchDataType(JNode node)
5358
{
54-
if(Runtime.TryExecute(() => CheckDataType(node))) return true;
55-
DataTypes.Where(d => !d.Nested).ForEach(d => d.MatchForReport(node));
56-
DataTypes.Where(d => d.Nested).ForEach(d => d.MatchForReport(node));
59+
if(Runtime.Exceptions.TryExecute(() => CheckDataType(node))) return true;
60+
SaveTryBuffer();
61+
foreach(var e in _exceptions) FailWith(e);
5762
return false;
5863
}
5964

65+
private static IEnumerable<Exception> ProcessTryBuffer(List<Exception> buffer) {
66+
var list = new List<Exception>(buffer.Count);
67+
foreach(var e in buffer)
68+
{
69+
var result = MergeException(TryGetLast(list), e);
70+
if(result is not null) list[^1] = result;
71+
else list.Add(e);
72+
}
73+
return list;
74+
}
75+
76+
private static JsonSchemaException? MergeException(Exception? ex1, Exception? ex2) {
77+
if(ex1 is not JsonSchemaException e1) return null;
78+
if(ex2 is not JsonSchemaException e2) return null;
79+
if(e1.Code != e2.Code) return null;
80+
var a1 = e1.GetAttribute(DataTypeName);
81+
var a2 = e2.GetAttribute(DataTypeName);
82+
if(a1 is null || a2 is null) return null;
83+
var result = new JsonSchemaException(e1.Error, MergeExpected(e1, e2), e2.Actual);
84+
result.SetAttribute(DataTypeName, a1 + a2);
85+
return result;
86+
}
87+
88+
private static ExpectedDetail MergeExpected(JsonSchemaException ex1,
89+
JsonSchemaException ex2) {
90+
var typeName2 = ex2.GetAttribute(DataTypeName);
91+
var expected1 = ex1.Expected;
92+
return new ExpectedDetail(expected1.Context, $"{expected1.Message} or {typeName2}");
93+
}
94+
6095
private bool CheckDataType(JNode node)
6196
{
62-
var list1 = DataTypes.Where(d => !d.Nested).Select(d => d.Match(node)).ToList();
63-
var result1 = list1.AnyTrue();
64-
var list2 = DataTypes.Where(d => d.Nested && (d.IsApplicable(node) || !result1))
65-
.Select(d => d.Match(node)).ToList();
66-
var result2 = list2.AnyTrue() || list2.Count == 0;
67-
return (result1 || list1.Count == 0) && result2;
97+
var list1 = DataTypes.Where(d => !d.Nested).ToList();
98+
var result1 = AnyMatch(list1, node);
99+
if(result1) TryBuffer.Clear();
100+
var list2 = DataTypes.Where(d => d.Nested && (d.IsApplicable(node) || !result1)).ToList();
101+
if(list2.IsEmpty()) return result1 || list1.IsEmpty();
102+
if(node is not JComposite composite) return FailWith(
103+
new JsonSchemaException(
104+
new ErrorDetail(DTYP03, InvalidNonCompositeType),
105+
ExpectedDetail.AsInvalidNonCompositeType(list2.First()),
106+
ActualDetail.AsInvalidNonCompositeType(node)));
107+
SaveTryBuffer();
108+
var result2 = composite.Components.ForEachTrue(n => AnyMatch(list2, n));
109+
return (result1 || list1.IsEmpty()) && result2;
110+
}
111+
112+
private bool AnyMatch(List<JDataType> list, JNode node) {
113+
TryBuffer.Clear();
114+
foreach(var d in list) if(d.Match(node)) return true;
115+
SaveTryBuffer();
116+
return false;
117+
}
118+
119+
private void SaveTryBuffer() {
120+
if(TryBuffer.IsEmpty()) return;
121+
_exceptions.AddRange(ProcessTryBuffer(TryBuffer));
122+
TryBuffer.Clear();
68123
}
69124

70125
public override string ToString() => (

0 commit comments

Comments
 (0)