Skip to content

Commit e25278e

Browse files
committed
big refactor
1 parent 41dff05 commit e25278e

37 files changed

+2881
-3297
lines changed

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
- Follow the sequence plan → implement → verify; do not pivot without restating the plan.
88
- Stop immediately on unexpected failures and ask before changing approach.
99
- Keep edits atomic and avoid leaving mixed partial states.
10-
- Propose options with trade-offs before invasive changes.
10+
- Propose jsonSchemaOptions with trade-offs before invasive changes.
1111
- Prefer mechanical, reversible transforms (especially when syncing upstream sources).
1212
- Validate that outputs are non-empty before overwriting files.
1313
- Minimal shims are acceptable only when needed to keep backports compiling.
@@ -287,7 +287,7 @@ git push -u origin "rel-$VERSION" && echo "✅ Success" || echo "🛑 Unable to
287287
2. **MVF Flow (Mermaid)**
288288
```mermaid
289289
flowchart TD
290-
A[compile(initialDoc, initialUri, options)] --> B[Work Stack (LIFO)]
290+
A[compile(initialDoc, initialUri, jsonSchemaOptions)] --> B[Work Stack (LIFO)]
291291
B -->|push initialUri| C{pop docUri}
292292
C -->|empty| Z[freeze Roots (immutable) → return primary root facade]
293293
C --> D[fetch/parse JSON for docUri]
@@ -334,7 +334,7 @@ type RefToken =
334334
| { kind: "Local"; pointer: JsonPointer }
335335
| { kind: "Remote"; doc: DocURI; pointer: JsonPointer };
336336

337-
function compile(initialDoc: unknown, initialUri: DocURI, options?: unknown): {
337+
function compile(initialDoc: unknown, initialUri: DocURI, jsonSchemaOptions?: unknown): {
338338
primary: Root;
339339
roots: Roots; // unused by MVF runtime; ready for remote expansions
340340
} {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.github.simbo1905.json.schema;
2+
3+
import jdk.sandbox.java.util.json.JsonValue;
4+
5+
import java.util.Deque;
6+
import java.util.List;
7+
8+
/// AllOf composition - must satisfy all schemas
9+
public record AllOfSchema(List<JsonSchema> schemas) implements JsonSchema {
10+
@Override
11+
public ValidationResult validateAt(String path, JsonValue json, Deque<ValidationFrame> stack) {
12+
// Push all subschemas onto the stack for validation
13+
for (JsonSchema schema : schemas) {
14+
stack.push(new ValidationFrame(path, schema, json));
15+
}
16+
return ValidationResult.success(); // Actual results emerge from stack processing
17+
}
18+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.github.simbo1905.json.schema;
2+
3+
import jdk.sandbox.java.util.json.JsonValue;
4+
5+
import java.util.ArrayDeque;
6+
import java.util.ArrayList;
7+
import java.util.Deque;
8+
import java.util.List;
9+
10+
/// AnyOf composition - must satisfy at least one schema
11+
public record AnyOfSchema(List<JsonSchema> schemas) implements JsonSchema {
12+
@Override
13+
public ValidationResult validateAt(String path, JsonValue json, Deque<ValidationFrame> stack) {
14+
List<ValidationError> collected = new ArrayList<>();
15+
boolean anyValid = false;
16+
17+
for (JsonSchema schema : schemas) {
18+
// Create a separate validation stack for this branch
19+
Deque<ValidationFrame> branchStack = new ArrayDeque<>();
20+
List<ValidationError> branchErrors = new ArrayList<>();
21+
22+
LOG.finest(() -> "BRANCH START: " + schema.getClass().getSimpleName());
23+
branchStack.push(new ValidationFrame(path, schema, json));
24+
25+
while (!branchStack.isEmpty()) {
26+
ValidationFrame frame = branchStack.pop();
27+
ValidationResult result = frame.schema().validateAt(frame.path(), frame.json(), branchStack);
28+
if (!result.valid()) {
29+
branchErrors.addAll(result.errors());
30+
}
31+
}
32+
33+
if (branchErrors.isEmpty()) {
34+
anyValid = true;
35+
break;
36+
}
37+
collected.addAll(branchErrors);
38+
LOG.finest(() -> "BRANCH END: " + branchErrors.size() + " errors");
39+
}
40+
41+
return anyValid ? ValidationResult.success() : ValidationResult.failure(collected);
42+
}
43+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.github.simbo1905.json.schema;
2+
3+
import jdk.sandbox.java.util.json.JsonValue;
4+
5+
import java.util.Deque;
6+
7+
/// Any schema - accepts all values
8+
public record AnySchema() implements JsonSchema {
9+
static final io.github.simbo1905.json.schema.AnySchema INSTANCE = new io.github.simbo1905.json.schema.AnySchema();
10+
11+
@Override
12+
public ValidationResult validateAt(String path, JsonValue json, Deque<ValidationFrame> stack) {
13+
return ValidationResult.success();
14+
}
15+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package io.github.simbo1905.json.schema;
2+
3+
import jdk.sandbox.java.util.json.JsonArray;
4+
import jdk.sandbox.java.util.json.JsonObject;
5+
import jdk.sandbox.java.util.json.JsonString;
6+
import jdk.sandbox.java.util.json.JsonValue;
7+
8+
import java.util.*;
9+
10+
/// Array schema with item validation and constraints
11+
public record ArraySchema(
12+
JsonSchema items,
13+
Integer minItems,
14+
Integer maxItems,
15+
Boolean uniqueItems,
16+
// NEW: Pack 2 array features
17+
List<JsonSchema> prefixItems,
18+
JsonSchema contains,
19+
Integer minContains,
20+
Integer maxContains
21+
) implements JsonSchema {
22+
23+
@Override
24+
public ValidationResult validateAt(String path, JsonValue json, Deque<ValidationFrame> stack) {
25+
if (!(json instanceof JsonArray arr)) {
26+
return ValidationResult.failure(List.of(
27+
new ValidationError(path, "Expected array")
28+
));
29+
}
30+
31+
List<ValidationError> errors = new ArrayList<>();
32+
int itemCount = arr.values().size();
33+
34+
// Check item count constraints
35+
if (minItems != null && itemCount < minItems) {
36+
errors.add(new ValidationError(path, "Too few items: expected at least " + minItems));
37+
}
38+
if (maxItems != null && itemCount > maxItems) {
39+
errors.add(new ValidationError(path, "Too many items: expected at most " + maxItems));
40+
}
41+
42+
// Check uniqueness if required (structural equality)
43+
if (uniqueItems != null && uniqueItems) {
44+
Set<String> seen = new HashSet<>();
45+
for (JsonValue item : arr.values()) {
46+
String canonicalKey = canonicalize(item);
47+
if (!seen.add(canonicalKey)) {
48+
errors.add(new ValidationError(path, "Array items must be unique"));
49+
break;
50+
}
51+
}
52+
}
53+
54+
// Validate prefixItems + items (tuple validation)
55+
if (prefixItems != null && !prefixItems.isEmpty()) {
56+
// Validate prefix items - fail if not enough items for all prefix positions
57+
for (int i = 0; i < prefixItems.size(); i++) {
58+
if (i >= itemCount) {
59+
errors.add(new ValidationError(path, "Array has too few items for prefixItems validation"));
60+
break;
61+
}
62+
String itemPath = path + "[" + i + "]";
63+
// Validate prefix items immediately to capture errors
64+
ValidationResult prefixResult = prefixItems.get(i).validateAt(itemPath, arr.values().get(i), stack);
65+
if (!prefixResult.valid()) {
66+
errors.addAll(prefixResult.errors());
67+
}
68+
}
69+
// Validate remaining items with items schema if present
70+
if (items != null && items != AnySchema.INSTANCE) {
71+
for (int i = prefixItems.size(); i < itemCount; i++) {
72+
String itemPath = path + "[" + i + "]";
73+
stack.push(new ValidationFrame(itemPath, items, arr.values().get(i)));
74+
}
75+
}
76+
} else if (items != null && items != AnySchema.INSTANCE) {
77+
// Original items validation (no prefixItems)
78+
int index = 0;
79+
for (JsonValue item : arr.values()) {
80+
String itemPath = path + "[" + index + "]";
81+
stack.push(new ValidationFrame(itemPath, items, item));
82+
index++;
83+
}
84+
}
85+
86+
// Validate contains / minContains / maxContains
87+
if (contains != null) {
88+
int matchCount = 0;
89+
for (JsonValue item : arr.values()) {
90+
// Create isolated validation to check if item matches contains schema
91+
Deque<ValidationFrame> tempStack = new ArrayDeque<>();
92+
List<ValidationError> tempErrors = new ArrayList<>();
93+
tempStack.push(new ValidationFrame("", contains, item));
94+
95+
while (!tempStack.isEmpty()) {
96+
ValidationFrame frame = tempStack.pop();
97+
ValidationResult result = frame.schema().validateAt(frame.path(), frame.json(), tempStack);
98+
if (!result.valid()) {
99+
tempErrors.addAll(result.errors());
100+
}
101+
}
102+
103+
if (tempErrors.isEmpty()) {
104+
matchCount++;
105+
}
106+
}
107+
108+
int min = (minContains != null ? minContains : 1); // default min=1
109+
int max = (maxContains != null ? maxContains : Integer.MAX_VALUE); // default max=∞
110+
111+
if (matchCount < min) {
112+
errors.add(new ValidationError(path, "Array must contain at least " + min + " matching element(s)"));
113+
} else if (matchCount > max) {
114+
errors.add(new ValidationError(path, "Array must contain at most " + max + " matching element(s)"));
115+
}
116+
}
117+
118+
return errors.isEmpty() ? ValidationResult.success() : ValidationResult.failure(errors);
119+
}
120+
121+
/// Canonicalization helper for structural equality in uniqueItems
122+
static String canonicalize(JsonValue v) {
123+
switch (v) {
124+
case JsonObject o -> {
125+
var keys = new ArrayList<>(o.members().keySet());
126+
Collections.sort(keys);
127+
var sb = new StringBuilder("{");
128+
for (int i = 0; i < keys.size(); i++) {
129+
String k = keys.get(i);
130+
if (i > 0) sb.append(',');
131+
sb.append('"').append(escapeJsonString(k)).append("\":").append(canonicalize(o.members().get(k)));
132+
}
133+
return sb.append('}').toString();
134+
}
135+
case JsonArray a -> {
136+
var sb = new StringBuilder("[");
137+
for (int i = 0; i < a.values().size(); i++) {
138+
if (i > 0) sb.append(',');
139+
sb.append(canonicalize(a.values().get(i)));
140+
}
141+
return sb.append(']').toString();
142+
}
143+
case JsonString s -> {
144+
return "\"" + escapeJsonString(s.value()) + "\"";
145+
}
146+
case null, default -> {
147+
// numbers/booleans/null: rely on stable toString from the Json* impls
148+
assert v != null;
149+
return v.toString();
150+
}
151+
}
152+
}
153+
static String escapeJsonString(String s) {
154+
if (s == null) return "null";
155+
StringBuilder result = new StringBuilder();
156+
for (int i = 0; i < s.length(); i++) {
157+
char ch = s.charAt(i);
158+
switch (ch) {
159+
case '"':
160+
result.append("\\\"");
161+
break;
162+
case '\\':
163+
result.append("\\\\");
164+
break;
165+
case '\b':
166+
result.append("\\b");
167+
break;
168+
case '\f':
169+
result.append("\\f");
170+
break;
171+
case '\n':
172+
result.append("\\n");
173+
break;
174+
case '\r':
175+
result.append("\\r");
176+
break;
177+
case '\t':
178+
result.append("\\t");
179+
break;
180+
default:
181+
if (ch < 0x20 || ch > 0x7e) {
182+
result.append("\\u").append(String.format("%04x", (int) ch));
183+
} else {
184+
result.append(ch);
185+
}
186+
}
187+
}
188+
return result.toString();
189+
}
190+
191+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.github.simbo1905.json.schema;
2+
3+
import jdk.sandbox.java.util.json.JsonBoolean;
4+
import jdk.sandbox.java.util.json.JsonValue;
5+
6+
import java.util.Deque;
7+
import java.util.List;
8+
9+
/// Boolean schema - validates boolean values
10+
public record BooleanSchema() implements JsonSchema {
11+
/// Singleton instances for boolean sub-schema handling
12+
static final io.github.simbo1905.json.schema.BooleanSchema TRUE = new io.github.simbo1905.json.schema.BooleanSchema();
13+
static final io.github.simbo1905.json.schema.BooleanSchema FALSE = new io.github.simbo1905.json.schema.BooleanSchema();
14+
15+
@Override
16+
public ValidationResult validateAt(String path, JsonValue json, Deque<ValidationFrame> stack) {
17+
// For boolean subschemas, FALSE always fails, TRUE always passes
18+
if (this == FALSE) {
19+
return ValidationResult.failure(List.of(
20+
new ValidationError(path, "Schema should not match")
21+
));
22+
}
23+
if (this == TRUE) {
24+
return ValidationResult.success();
25+
}
26+
// Regular boolean validation for normal boolean schemas
27+
if (!(json instanceof JsonBoolean)) {
28+
return ValidationResult.failure(List.of(
29+
new ValidationError(path, "Expected boolean")
30+
));
31+
}
32+
return ValidationResult.success();
33+
}
34+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.github.simbo1905.json.schema;
2+
3+
import jdk.sandbox.java.util.json.JsonValue;
4+
5+
import java.util.Deque;
6+
7+
/// If/Then/Else conditional schema
8+
public record ConditionalSchema(JsonSchema ifSchema, JsonSchema thenSchema,
9+
JsonSchema elseSchema) implements JsonSchema {
10+
@Override
11+
public ValidationResult validateAt(String path, JsonValue json, Deque<ValidationFrame> stack) {
12+
// Step 1 - evaluate IF condition (still needs direct validation)
13+
ValidationResult ifResult = ifSchema.validate(json);
14+
15+
// Step 2 - choose branch
16+
JsonSchema branch = ifResult.valid() ? thenSchema : elseSchema;
17+
18+
LOG.finer(() -> String.format(
19+
"Conditional path=%s ifValid=%b branch=%s",
20+
path, ifResult.valid(),
21+
branch == null ? "none" : (ifResult.valid() ? "then" : "else")));
22+
23+
// Step 3 - if there's a branch, push it onto the stack for later evaluation
24+
if (branch == null) {
25+
return ValidationResult.success(); // no branch → accept
26+
}
27+
28+
// NEW: push branch onto SAME stack instead of direct call
29+
stack.push(new ValidationFrame(path, branch, json));
30+
return ValidationResult.success(); // real result emerges later
31+
}
32+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.github.simbo1905.json.schema;
2+
3+
import jdk.sandbox.java.util.json.JsonValue;
4+
5+
import java.util.Deque;
6+
import java.util.List;
7+
8+
/// Const schema - validates that a value equals a constant
9+
public record ConstSchema(JsonValue constValue) implements JsonSchema {
10+
@Override
11+
public ValidationResult validateAt(String path, JsonValue json, Deque<ValidationFrame> stack) {
12+
return json.equals(constValue) ?
13+
ValidationResult.success() :
14+
ValidationResult.failure(List.of(new ValidationError(path, "Value must equal const value")));
15+
}
16+
}

0 commit comments

Comments
 (0)