Skip to content

Commit d8ca22a

Browse files
committed
pack6
1 parent efd25bd commit d8ca22a

File tree

4 files changed

+377
-64
lines changed

4 files changed

+377
-64
lines changed

json-java21-schema/debug.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import jdk.sandbox.java.util.json.Json;
2+
import io.github.simbo1905.json.schema.JsonSchema;
3+
4+
public class Debug {
5+
public static void main(String[] args) {
6+
var schemaJson = Json.parse("""
7+
{
8+
"$defs": {
9+
"deny": false,
10+
"allow": true
11+
},
12+
"one": { "$ref":"#/$defs/allow" },
13+
"two": { "$ref":"#/$defs/deny" }
14+
}
15+
""");
16+
17+
try {
18+
var schema = JsonSchema.compile(schemaJson);
19+
System.out.println("Schema compiled successfully!");
20+
} catch (Exception e) {
21+
System.out.println("Error: " + e.getMessage());
22+
e.printStackTrace();
23+
}
24+
}
25+
}

json-java21-schema/src/main/java/io/github/simbo1905/json/schema/JsonSchema.java

Lines changed: 146 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -516,10 +516,14 @@ public ValidationResult validateAt(String path, JsonValue json, Deque<Validation
516516
}
517517

518518
/// Reference schema for JSON Schema $ref
519-
record RefSchema(String ref) implements JsonSchema {
519+
record RefSchema(String ref, java.util.function.Supplier<JsonSchema> targetSupplier) implements JsonSchema {
520520
@Override
521521
public ValidationResult validateAt(String path, JsonValue json, Deque<ValidationFrame> stack) {
522-
throw new UnsupportedOperationException("$ref resolution not implemented");
522+
JsonSchema target = targetSupplier.get();
523+
if (target == null) {
524+
return ValidationResult.failure(List.of(new ValidationError(path, "Unresolved $ref: " + ref)));
525+
}
526+
return target.validateAt(path, json, stack);
523527
}
524528
}
525529

@@ -757,6 +761,9 @@ final class SchemaCompiler {
757761
private static final Map<String, JsonSchema> definitions = new HashMap<>();
758762
private static JsonSchema currentRootSchema;
759763
private static Options currentOptions;
764+
private static final Map<String, JsonSchema> compiledByPointer = new HashMap<>();
765+
private static final Map<String, JsonValue> rawByPointer = new HashMap<>();
766+
private static final Deque<String> resolutionStack = new ArrayDeque<>();
760767

761768
private static void trace(String stage, JsonValue fragment) {
762769
if (LOG.isLoggable(Level.FINER)) {
@@ -765,12 +772,124 @@ private static void trace(String stage, JsonValue fragment) {
765772
}
766773
}
767774

775+
/// JSON Pointer utility for RFC-6901 fragment navigation
776+
static Optional<JsonValue> navigatePointer(JsonValue root, String pointer) {
777+
if (pointer.isEmpty() || pointer.equals("#")) {
778+
return Optional.of(root);
779+
}
780+
781+
// Remove leading # if present
782+
String path = pointer.startsWith("#") ? pointer.substring(1) : pointer;
783+
if (path.isEmpty()) {
784+
return Optional.of(root);
785+
}
786+
787+
// Must start with /
788+
if (!path.startsWith("/")) {
789+
return Optional.empty();
790+
}
791+
792+
JsonValue current = root;
793+
String[] tokens = path.substring(1).split("/");
794+
795+
for (String token : tokens) {
796+
// Unescape ~1 -> / and ~0 -> ~
797+
String unescaped = token.replace("~1", "/").replace("~0", "~");
798+
799+
if (current instanceof JsonObject obj) {
800+
current = obj.members().get(unescaped);
801+
if (current == null) {
802+
return Optional.empty();
803+
}
804+
} else if (current instanceof JsonArray arr) {
805+
try {
806+
int index = Integer.parseInt(unescaped);
807+
if (index < 0 || index >= arr.values().size()) {
808+
return Optional.empty();
809+
}
810+
current = arr.values().get(index);
811+
} catch (NumberFormatException e) {
812+
return Optional.empty();
813+
}
814+
} else {
815+
return Optional.empty();
816+
}
817+
}
818+
819+
return Optional.of(current);
820+
}
821+
822+
/// Resolve $ref with cycle detection and memoization
823+
static JsonSchema resolveRef(String ref) {
824+
// Check for cycles
825+
if (resolutionStack.contains(ref)) {
826+
throw new IllegalArgumentException("Cyclic $ref: " + String.join(" -> ", resolutionStack) + " -> " + ref);
827+
}
828+
829+
// Check memoized results
830+
JsonSchema cached = compiledByPointer.get(ref);
831+
if (cached != null) {
832+
return cached;
833+
}
834+
835+
if (ref.equals("#")) {
836+
// Root reference - return RootRef instead of RefSchema to avoid cycles
837+
return new RootRef(() -> currentRootSchema);
838+
}
839+
840+
// Resolve via JSON Pointer
841+
Optional<JsonValue> target = navigatePointer(rawByPointer.get(""), ref);
842+
if (target.isEmpty()) {
843+
throw new IllegalArgumentException("Unresolved $ref: " + ref);
844+
}
845+
846+
// Check if it's a boolean schema
847+
JsonValue targetValue = target.get();
848+
if (targetValue instanceof JsonBoolean bool) {
849+
JsonSchema schema = bool.value() ? AnySchema.INSTANCE : new NotSchema(AnySchema.INSTANCE);
850+
compiledByPointer.put(ref, schema);
851+
return new RefSchema(ref, () -> schema);
852+
}
853+
854+
// Push to resolution stack for cycle detection
855+
resolutionStack.push(ref);
856+
try {
857+
JsonSchema compiled = compileInternal(targetValue);
858+
compiledByPointer.put(ref, compiled);
859+
final JsonSchema finalCompiled = compiled;
860+
return new RefSchema(ref, () -> finalCompiled);
861+
} finally {
862+
resolutionStack.pop();
863+
}
864+
}
865+
866+
/// Index schema fragments by JSON Pointer for efficient lookup
867+
static void indexSchemaByPointer(String pointer, JsonValue value) {
868+
rawByPointer.put(pointer, value);
869+
870+
if (value instanceof JsonObject obj) {
871+
for (var entry : obj.members().entrySet()) {
872+
String key = entry.getKey();
873+
// Escape special characters in key
874+
String escapedKey = key.replace("~", "~0").replace("/", "~1");
875+
indexSchemaByPointer(pointer + "/" + escapedKey, entry.getValue());
876+
}
877+
} else if (value instanceof JsonArray arr) {
878+
for (int i = 0; i < arr.values().size(); i++) {
879+
indexSchemaByPointer(pointer + "/" + i, arr.values().get(i));
880+
}
881+
}
882+
}
883+
768884
static JsonSchema compile(JsonValue schemaJson) {
769885
return compile(schemaJson, Options.DEFAULT);
770886
}
771887

772888
static JsonSchema compile(JsonValue schemaJson, Options options) {
773889
definitions.clear(); // Clear any previous definitions
890+
compiledByPointer.clear();
891+
rawByPointer.clear();
892+
resolutionStack.clear();
774893
currentRootSchema = null;
775894
currentOptions = options;
776895

@@ -794,6 +913,9 @@ static JsonSchema compile(JsonValue schemaJson, Options options) {
794913
// Update options with final assertion setting
795914
currentOptions = new Options(assertFormats);
796915

916+
// Index the raw schema by JSON Pointer
917+
indexSchemaByPointer("", schemaJson);
918+
797919
trace("compile-start", schemaJson);
798920
JsonSchema schema = compileInternal(schemaJson);
799921
currentRootSchema = schema; // Store the root schema for self-references
@@ -814,7 +936,10 @@ private static JsonSchema compileInternal(JsonValue schemaJson) {
814936
if (defsValue instanceof JsonObject defsObj) {
815937
trace("compile-defs", defsValue);
816938
for (var entry : defsObj.members().entrySet()) {
817-
definitions.put("#/$defs/" + entry.getKey(), compileInternal(entry.getValue()));
939+
String pointer = "#/$defs/" + entry.getKey();
940+
JsonSchema compiled = compileInternal(entry.getValue());
941+
definitions.put(pointer, compiled);
942+
compiledByPointer.put(pointer, compiled);
818943
}
819944
}
820945

@@ -823,15 +948,7 @@ private static JsonSchema compileInternal(JsonValue schemaJson) {
823948
if (refValue instanceof JsonString refStr) {
824949
String ref = refStr.value();
825950
trace("compile-ref", refValue);
826-
if (ref.equals("#")) {
827-
// Lazily resolve to whatever the root schema becomes after compilation
828-
return new RootRef(() -> currentRootSchema);
829-
}
830-
JsonSchema resolved = definitions.get(ref);
831-
if (resolved == null) {
832-
throw new IllegalArgumentException("Unresolved $ref: " + ref);
833-
}
834-
return resolved;
951+
return resolveRef(ref);
835952
}
836953

837954
// Handle composition keywords
@@ -1270,14 +1387,30 @@ public ValidationResult validateAt(String path, JsonValue json, Deque<Validation
12701387

12711388
/// Root reference schema that refers back to the root schema
12721389
record RootRef(java.util.function.Supplier<JsonSchema> rootSupplier) implements JsonSchema {
1390+
// Track recursion depth per thread to avoid infinite loops
1391+
private static final ThreadLocal<Integer> recursionDepth = ThreadLocal.withInitial(() -> 0);
1392+
private static final int MAX_RECURSION_DEPTH = 50;
1393+
12731394
@Override
12741395
public ValidationResult validateAt(String path, JsonValue json, Deque<ValidationFrame> stack) {
12751396
JsonSchema root = rootSupplier.get();
12761397
if (root == null) {
12771398
// No root yet (should not happen during validation), accept for now
12781399
return ValidationResult.success();
12791400
}
1280-
return root.validate(json); // Direct validation against root schema
1401+
1402+
// Check recursion depth to prevent infinite loops
1403+
int depth = recursionDepth.get();
1404+
if (depth >= MAX_RECURSION_DEPTH) {
1405+
return ValidationResult.success(); // Break the cycle
1406+
}
1407+
1408+
try {
1409+
recursionDepth.set(depth + 1);
1410+
return root.validate(json);
1411+
} finally {
1412+
recursionDepth.set(depth);
1413+
}
12811414
}
12821415
}
12831416

json-java21-schema/src/test/java/io/github/simbo1905/json/schema/DebugFormatTest.java

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

0 commit comments

Comments
 (0)