Skip to content

Commit 792bfd7

Browse files
committed
wip
1 parent 87a354c commit 792bfd7

File tree

2 files changed

+352
-1
lines changed

2 files changed

+352
-1
lines changed

json-java21-jtd/src/main/java/json/java21/jtd/Jtd.java

Lines changed: 198 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package json.java21.jtd;
22

33
import jdk.sandbox.java.util.json.*;
4+
import jdk.sandbox.internal.util.json.*;
45

56
import java.util.ArrayList;
67
import java.util.Collections;
@@ -14,6 +15,45 @@ public class Jtd {
1415

1516
private static final Logger LOG = Logger.getLogger(Jtd.class.getName());
1617

18+
/// Stack frame for iterative validation with path and offset tracking
19+
record Frame(JtdSchema schema, JsonValue instance, String ptr, Crumbs crumbs) {}
20+
21+
/// Lightweight breadcrumb trail for human-readable error paths
22+
record Crumbs(String value) {
23+
static Crumbs root() {
24+
return new Crumbs("#");
25+
}
26+
27+
Crumbs withObjectField(String name) {
28+
return new Crumbs(value + "→field:" + name);
29+
}
30+
31+
Crumbs withArrayIndex(int idx) {
32+
return new Crumbs(value + "→item:" + idx);
33+
}
34+
}
35+
36+
/// Extracts offset from JsonValue implementation classes
37+
static int offsetOf(JsonValue v) {
38+
return switch (v) {
39+
case JsonObjectImpl j -> j.offset();
40+
case JsonArrayImpl j -> j.offset();
41+
case JsonStringImpl j -> j.offset();
42+
case JsonNumberImpl j -> j.offset();
43+
case JsonBooleanImpl j -> j.offset();
44+
case JsonNullImpl j -> j.offset();
45+
default -> -1; // unknown/foreign implementation
46+
};
47+
}
48+
49+
/// Creates an enriched error message with offset and path information
50+
static String enrichedError(String baseMessage, Frame frame, JsonValue contextValue) {
51+
int off = offsetOf(contextValue);
52+
String ptr = frame.ptr;
53+
String via = frame.crumbs.value();
54+
return "[off=" + off + " ptr=" + ptr + " via=" + via + "] " + baseMessage;
55+
}
56+
1757
/// Validates a JSON instance against a JTD schema
1858
/// @param schema The JTD schema as a JsonValue
1959
/// @param instance The JSON instance to validate
@@ -23,7 +63,7 @@ public Result validate(JsonValue schema, JsonValue instance) {
2363

2464
try {
2565
JtdSchema jtdSchema = compileSchema(schema);
26-
Result result = jtdSchema.validate(instance);
66+
Result result = validateWithStack(jtdSchema, instance);
2767

2868
LOG.fine(() -> "JTD validation result: " + (result.isValid() ? "VALID" : "INVALID") +
2969
", errors: " + result.errors().size());
@@ -35,6 +75,163 @@ public Result validate(JsonValue schema, JsonValue instance) {
3575
}
3676
}
3777

78+
/// Validates using iterative stack-based approach with offset and path tracking
79+
Result validateWithStack(JtdSchema schema, JsonValue instance) {
80+
List<String> errors = new ArrayList<>();
81+
java.util.Deque<Frame> stack = new java.util.ArrayDeque<>();
82+
83+
// Push initial frame
84+
Frame rootFrame = new Frame(schema, instance, "#", Crumbs.root());
85+
stack.push(rootFrame);
86+
87+
LOG.fine(() -> "Starting stack validation - initial frame: " + rootFrame);
88+
89+
// Process frames iteratively
90+
while (!stack.isEmpty()) {
91+
Frame frame = stack.pop();
92+
LOG.fine(() -> "Processing frame - schema: " + frame.schema.getClass().getSimpleName() +
93+
", ptr: " + frame.ptr + ", off: " + offsetOf(frame.instance));
94+
95+
// Validate current frame
96+
if (!frame.schema.validateWithFrame(frame, errors, false)) {
97+
LOG.fine(() -> "Validation failed for frame at " + frame.ptr + " with " + errors.size() + " errors");
98+
continue; // Continue processing other frames even if this one failed
99+
}
100+
101+
// Handle special validations for PropertiesSchema
102+
if (frame.schema instanceof JtdSchema.PropertiesSchema propsSchema) {
103+
validatePropertiesSchema(frame, propsSchema, errors);
104+
}
105+
106+
// Push child frames based on schema type
107+
pushChildFrames(frame, stack);
108+
}
109+
110+
return errors.isEmpty() ? Result.success() : Result.failure(errors);
111+
}
112+
113+
/// Validates PropertiesSchema-specific rules (missing required, additional properties)
114+
void validatePropertiesSchema(Frame frame, JtdSchema.PropertiesSchema propsSchema, List<String> errors) {
115+
JsonValue instance = frame.instance();
116+
if (!(instance instanceof JsonObject obj)) {
117+
return; // Type validation should have already caught this
118+
}
119+
120+
// Check for missing required properties
121+
for (var entry : propsSchema.properties().entrySet()) {
122+
String key = entry.getKey();
123+
JsonValue value = obj.members().get(key);
124+
125+
if (value == null) {
126+
// Missing required property - create error with containing object offset
127+
String error = Jtd.Error.MISSING_REQUIRED_PROPERTY.message(key);
128+
String enrichedError = Jtd.enrichedError(error, frame, instance);
129+
errors.add(enrichedError);
130+
LOG.fine(() -> "Missing required property: " + enrichedError);
131+
}
132+
}
133+
134+
// Check for additional properties if not allowed
135+
if (!propsSchema.additionalProperties()) {
136+
for (String key : obj.members().keySet()) {
137+
if (!propsSchema.properties().containsKey(key) && !propsSchema.optionalProperties().containsKey(key)) {
138+
JsonValue value = obj.members().get(key);
139+
// Additional property not allowed - create error with the value's offset
140+
String error = Jtd.Error.ADDITIONAL_PROPERTY_NOT_ALLOWED.message(key);
141+
String enrichedError = Jtd.enrichedError(error, frame, value);
142+
errors.add(enrichedError);
143+
LOG.fine(() -> "Additional property not allowed: " + enrichedError);
144+
}
145+
}
146+
}
147+
}
148+
149+
/// Pushes child frames for complex schema types
150+
void pushChildFrames(Frame frame, java.util.Deque<Frame> stack) {
151+
JtdSchema schema = frame.schema;
152+
JsonValue instance = frame.instance;
153+
154+
LOG.finer(() -> "Pushing child frames for schema type: " + schema.getClass().getSimpleName());
155+
156+
switch (schema) {
157+
case JtdSchema.ElementsSchema elementsSchema -> {
158+
if (instance instanceof JsonArray arr) {
159+
int index = 0;
160+
for (JsonValue element : arr.values()) {
161+
String childPtr = frame.ptr + "/" + index;
162+
Crumbs childCrumbs = frame.crumbs.withArrayIndex(index);
163+
Frame childFrame = new Frame(elementsSchema.elements(), element, childPtr, childCrumbs);
164+
stack.push(childFrame);
165+
LOG.finer(() -> "Pushed array element frame at " + childPtr);
166+
index++;
167+
}
168+
}
169+
}
170+
case JtdSchema.PropertiesSchema propsSchema -> {
171+
if (instance instanceof JsonObject obj) {
172+
// Push required properties that are present
173+
for (var entry : propsSchema.properties().entrySet()) {
174+
String key = entry.getKey();
175+
JsonValue value = obj.members().get(key);
176+
177+
if (value != null) {
178+
String childPtr = frame.ptr + "/" + key;
179+
Crumbs childCrumbs = frame.crumbs.withObjectField(key);
180+
Frame childFrame = new Frame(entry.getValue(), value, childPtr, childCrumbs);
181+
stack.push(childFrame);
182+
LOG.finer(() -> "Pushed required property frame at " + childPtr);
183+
}
184+
}
185+
186+
// Push optional properties that are present
187+
for (var entry : propsSchema.optionalProperties().entrySet()) {
188+
String key = entry.getKey();
189+
JtdSchema childSchema = entry.getValue();
190+
JsonValue value = obj.members().get(key);
191+
192+
if (value != null) {
193+
String childPtr = frame.ptr + "/" + key;
194+
Crumbs childCrumbs = frame.crumbs.withObjectField(key);
195+
Frame childFrame = new Frame(childSchema, value, childPtr, childCrumbs);
196+
stack.push(childFrame);
197+
LOG.finer(() -> "Pushed optional property frame at " + childPtr);
198+
}
199+
}
200+
}
201+
}
202+
case JtdSchema.ValuesSchema valuesSchema -> {
203+
if (instance instanceof JsonObject obj) {
204+
for (var entry : obj.members().entrySet()) {
205+
String key = entry.getKey();
206+
JsonValue value = entry.getValue();
207+
String childPtr = frame.ptr + "/" + key;
208+
Crumbs childCrumbs = frame.crumbs.withObjectField(key);
209+
Frame childFrame = new Frame(valuesSchema.values(), value, childPtr, childCrumbs);
210+
stack.push(childFrame);
211+
LOG.finer(() -> "Pushed values schema frame at " + childPtr);
212+
}
213+
}
214+
}
215+
case JtdSchema.DiscriminatorSchema discSchema -> {
216+
if (instance instanceof JsonObject obj) {
217+
JsonValue discriminatorValue = obj.members().get(discSchema.discriminator());
218+
if (discriminatorValue instanceof JsonString discStr) {
219+
String discriminatorValueStr = discStr.value();
220+
JtdSchema variantSchema = discSchema.mapping().get(discriminatorValueStr);
221+
if (variantSchema != null) {
222+
// Push variant schema for validation
223+
Frame variantFrame = new Frame(variantSchema, instance, frame.ptr, frame.crumbs);
224+
stack.push(variantFrame);
225+
LOG.finer(() -> "Pushed discriminator variant frame for " + discriminatorValueStr);
226+
}
227+
}
228+
}
229+
}
230+
default -> // Simple schemas (Empty, Type, Enum, Nullable, Ref) don't push child frames
231+
LOG.finer(() -> "No child frames for schema type: " + schema.getClass().getSimpleName());
232+
}
233+
}
234+
38235
/// Compiles a JsonValue into a JtdSchema based on RFC 8927 rules
39236
JtdSchema compileSchema(JsonValue schema) {
40237
if (schema == null) {

0 commit comments

Comments
 (0)