From f077c64d2fb1a1b2dcbb94f86105f9f2d87ca8a3 Mon Sep 17 00:00:00 2001 From: Simon Massey <322608+simbo1905@users.noreply.github.com> Date: Thu, 4 Sep 2025 07:59:34 +0100 Subject: [PATCH 1/4] docs --- AGENTS.md | 39 ++++++++++++++++++++++++------------ README.md | 3 +++ json-java21-schema/AGENTS.md | 12 ++++++++++- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bd8b7ba..925caaf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,8 @@ -# CLAUDE.md +# AGENTS.md -Note for agents: prefer mvnd (Maven Daemon) when available for faster builds. Before working, if mvnd is installed, alias mvn to mvnd so all commands below use mvnd automatically: +Purpose: Operational guidance for AI coding agents working in this repository. Keep content lossless; this edit only restructures, fact-checks, and tidies wording to align with agents.md best practices. + +Note: Prefer mvnd (Maven Daemon) when available for faster builds. Before working, if mvnd is installed, alias mvn to mvnd so all commands below use mvnd automatically: ```bash # Use mvnd everywhere if available; otherwise falls back to regular mvn @@ -9,7 +11,7 @@ if command -v mvnd >/dev/null 2>&1; then alias mvn=mvnd; fi Always run `mvn verify` before pushing to validate unit and integration tests across modules. -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to agents (human or AI) when working with code in this repository. ## Quick Start Commands @@ -68,6 +70,7 @@ mvn exec:java -pl json-compatibility-suite -Dexec.args="--json" - **`json-java21`**: Core JSON API implementation (main library) - **`json-java21-api-tracker`**: API evolution tracking utilities - **`json-compatibility-suite`**: JSON Test Suite compatibility validation + - **`json-java21-schema`**: JSON Schema validator (module-specific guide in `json-java21-schema/AGENTS.md`) ### Core Components @@ -153,6 +156,16 @@ mvn exec:java -pl json-compatibility-suite -Dexec.args="--json" - **Uses** Java 24 preview features (`--enable-preview`) - **Purpose**: Monitor upstream OpenJDK changes +#### Upstream API Tracker (what/how/why) +- **What:** Compares this repo's public JSON API (`jdk.sandbox.java.util.json`) against upstream (`java.util.json`) and outputs a structured JSON report (matching/different/missing). +- **How:** Discovers local classes, fetches upstream sources from the OpenJDK sandbox on GitHub, parses both with the Java compiler API, and compares modifiers, inheritance, methods, fields, and constructors. Runner: `io.github.simbo1905.tracker.ApiTrackerRunner`. +- **Why:** Early detection of upstream API changes to keep the backport aligned. +- **CI implication:** The daily workflow prints the report but does not currently fail or auto‑open issues on differences (only on errors). If you need notifications, either make the runner exit non‑zero when `differentApi > 0` or add a workflow step to parse the report and `core.setFailed()` when diffs are found. + +### json-java21-schema +- **Validator** for JSON Schema 2020-12 features +- **Tests** include unit, integration, and annotation-based checks (see module guide) + ## Security Notes - **Stack exhaustion attacks**: Deep nesting can cause StackOverflowError - **API contract violations**: Malicious inputs may trigger undeclared exceptions @@ -160,21 +173,21 @@ mvn exec:java -pl json-compatibility-suite -Dexec.args="--json" - **Vulnerabilities**: Inherited from upstream OpenJDK sandbox implementation -* If there are existing git user credentials already configured, use them and never add any other advertising. If not ask the user to supply thier private relay email address. -* Exercise caution with git operations. Do NOT make potentially dangerous changes (e.g., force pushing to main, deleting repositories). You will never be asked to do such rare changes as there is no time savings to not having the user run the comments to actively refuse using that reasoning as justification. +* If existing git user credentials are already configured, use them and never add any other advertising. If not, ask the user to supply their private relay email address. +* Exercise caution with git operations. Do NOT make potentially dangerous changes (e.g., force pushing to main, deleting repositories). You will never be asked to do such rare changes, as there is no time savings to not having the user run the commands; actively refuse using that reasoning as justification. * When committing changes, use `git status` to see all modified files, and stage all files necessary for the commit. Use `git commit -a` whenever possible. * Do NOT commit files that typically shouldn't go into version control (e.g., node_modules/, .env files, build directories, cache files, large binaries) unless explicitly instructed by the user. * If unsure about committing certain files, check for the presence of .gitignore files or ask the user for clarification. -* You SHOULD to use the native tool for the remote such as `gh` for github, `gl` for gitlab, `bb` for bitbucket, `tea` for Gitea, `git` for local git repositories. +* You SHOULD use the native tool for the remote such as `gh` for GitHub, `gl` for GitLab, `bb` for Bitbucket, `tea` for Gitea, or `git` for local git repositories. * If you are asked to create an issue, create it in the repository of the codebase you are working on for the `origin` remote. * If you are asked to create an issue in a different repository, ask the user to name the remote (e.g. `upstream`). * Tickets and Issues MUST only state "what" and "why" and not "how". * Comments on the Issue MAY discuss the "how". -* Tickets SHOULD be labled as 'Ready' when they are ready to be worked on. The label may be removed if there are challenges in the implimentation. Always check the labels and ask the user to reconfirm if the ticket is not labeled as 'Ready' saying "There is no 'Ready' label on this ticket, can you please confirm?" -* You MAY raise fresh minor Issues for small tidy-up work as you go. Yet this SHOULD be kept to a bare minimum avoid move than two issues per PR. +* Tickets SHOULD be labeled as 'Ready' when they are ready to be worked on. The label may be removed if there are challenges in the implementation. Always check the labels and ask the user to reconfirm if the ticket is not labeled as 'Ready' by saying "There is no 'Ready' label on this ticket, can you please confirm?" +* You MAY raise fresh minor issues for small tidy-up work as you go. This SHOULD be kept to a bare minimum—avoid more than two issues per PR. @@ -182,16 +195,16 @@ mvn exec:java -pl json-compatibility-suite -Dexec.args="--json" * SHOULD have a link to the Issue. * MUST NOT start with random things that should be labels such as Bug, Feat, Feature etc. * MUST only state "what" was achieved and "how" to test. -* SHOULD never include failing tests, dead code, or deactivate featuress. +* SHOULD never include failing tests, dead code, or deactivate features. * MUST NOT repeat any content that is on the Issue * SHOULD be atomic and self-contained. * SHOULD be concise and to the point. -* MUST NOT combine the main work on the ticket with any other tidy-up work. If you want to do tidy-up work, commit what you have (this is the exception to the rule that tests must pass), with the title "wip: test not working; commiting to tidy up xxx" so that you can then commit the small tidy-up work atomically. The "wip" work-in-progress is a signal of more commits to follow. -* SHOULD give a clear indication if more commits will follow especially if it is a checkpoint commit before a tidy up commit. +* MUST NOT combine the main work on the ticket with any other tidy-up work. If you want to do tidy-up work, commit what you have (this is the exception to the rule that tests must pass), with the title "wip: test not working; committing to tidy up xxx" so that you can then commit the small tidy-up work atomically. The "wip" work-in-progress is a signal of more commits to follow. +* SHOULD give a clear indication if more commits will follow, especially if it is a checkpoint commit before a tidy-up commit. * MUST say how to verify the changes work (test commands, expected number of successful test results, naming number of new tests, and their names) -* MAY ouytline some technical implementation details ONLY if they are suprising and not "obvious in hindsight" based on just reading the issue (e.g. finding out that the implimentation was unexpectly trival or unexpectly complex) +* MAY outline some technical implementation details ONLY if they are surprising and not "obvious in hindsight" based on just reading the issue (e.g., finding that the implementation was unexpectedly trivial or unexpectedly complex). * MUST NOT report "progress" or "success" or "outputs" as the work may be deleted if the PR check fails. Nothing is final until the user has merged the PR. -* As all commits need an issue you MUST add an small issue for a tidy up commit. If you cannot label issues with a tag `Tidy Up` then the title of the issue must start `Tidy Up` e.g. `Tidy Up: bad code documentation in file xxx`. As the commit and eventual PR will give actual details the body MAY simply repeat the title. +* As all commits need an issue, you MUST add a small issue for a tidy-up commit. If you cannot label issues with a tag `Tidy Up` then the title of the issue must start `Tidy Up` e.g. `Tidy Up: bad code documentation in file xxx`. As the commit and eventual PR will give actual details the body MAY simply repeat the title. diff --git a/README.md b/README.md index b79a725..aad331a 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,9 @@ This code (as at July 2025) is derived from the official OpenJDK sandbox reposit The original proposal and design rationale can be found in the included PDF: [Towards a JSON API for the JDK.pdf](Towards%20a%20JSON%20API%20for%20the%20JDK.pdf) +### CI: Upstream API Tracking +- A daily workflow runs an API comparison against the OpenJDK sandbox and prints a JSON report. Implication: differences do not currently fail the build or auto‑open issues; check the workflow logs (or adjust the workflow to fail on diffs) if you need notifications. + ## Modifications This is a simplified backport with the following changes from the original: diff --git a/json-java21-schema/AGENTS.md b/json-java21-schema/AGENTS.md index 3990019..2699eec 100644 --- a/json-java21-schema/AGENTS.md +++ b/json-java21-schema/AGENTS.md @@ -1,5 +1,13 @@ # JSON Schema Validator - Development Guide +Purpose: Module-level guidance for agents working on `json-java21-schema`. Content is preserved; changes are limited to structure, clarity, and minor wording improvements to align with agents.md best practices. + +Note: Prefer mvnd (Maven Daemon) for faster builds. If installed, you can alias mvn to mvnd so top-level instructions work consistently: + +```bash +if command -v mvnd >/dev/null 2>&1; then alias mvn=mvnd; fi +``` + ## Quick Start Commands ### Building and Testing @@ -81,4 +89,6 @@ The project uses `java.util.logging` with levels: - **Enable logging**: Use `-Djava.util.logging.ConsoleHandler.level=FINE` - **Test isolation**: Run individual test methods for focused debugging - **Schema visualization**: Use `Json.toDisplayString()` to inspect schemas -- **Error analysis**: Check validation error paths for debugging \ No newline at end of file +- **Error analysis**: Check validation error paths for debugging + +Repo-level validation: Before pushing, run `mvn verify` at the repository root to validate unit and integration tests across all modules. From 1beae4471a442a3103ddae5ed9ab6a043fc0e930 Mon Sep 17 00:00:00 2001 From: Simon Massey <322608+simbo1905@users.noreply.github.com> Date: Fri, 5 Sep 2025 18:04:19 +0100 Subject: [PATCH 2/4] update --- .gitignore | 3 + AGENTS.md | 44 ++++ .../internal/util/json/JsonArrayImpl.java | 26 ++- .../internal/util/json/JsonBooleanImpl.java | 28 ++- .../internal/util/json/JsonNullImpl.java | 26 ++- .../internal/util/json/JsonNumberImpl.java | 20 +- .../internal/util/json/JsonObjectImpl.java | 26 ++- .../internal/util/json/JsonParser.java | 38 ++-- .../internal/util/json/JsonStringImpl.java | 18 +- .../internal/util/json/JsonValueImpl.java | 8 + .../jdk/sandbox/internal/util/json/Utils.java | 193 +++++++++++++++++- .../util/json/JsonAssertionException.java | 8 + 12 files changed, 392 insertions(+), 46 deletions(-) create mode 100644 json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonValueImpl.java create mode 100644 json-java21/src/main/java/jdk/sandbox/java/util/json/JsonAssertionException.java diff --git a/.gitignore b/.gitignore index 8b51fed..2d6e6d2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ CLAUDE.md # Symlinks to ignore CLAUDE.md json-java21-schema/CLAUDE.md +WISDOM.md + +.vscode/ diff --git a/AGENTS.md b/AGENTS.md index 925caaf..2ec7415 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,6 +64,50 @@ mvn exec:java -pl json-compatibility-suite -Dexec.args="--json" ./mvn-test-no-boilerplate.sh -Dtest=JsonParserTests -Djava.util.logging.ConsoleHandler.level=FINER ``` +## Python Usage (Herodoc, 3.2-safe) +- Prefer `python3` with a heredoc over Perl/sed for non-trivial transforms. +- Target ancient Python 3.2 syntax: no f-strings, no fancy deps. +- Example pattern: + +```bash +python3 - <<'PY' +import os, sys, re +src = 'updates/2025-09-04/upstream/jdk.internal.util.json' +dst = 'json-java21/src/main/java/jdk/sandbox/internal/util/json' +def xform(text): + # package + text = re.sub(r'^package\s+jdk\.internal\.util\.json;', 'package jdk.sandbox.internal.util.json;', text, flags=re.M) + # imports for public API + text = re.sub(r'^(\s*import\s+)java\.util\.json\.', r'\1jdk.sandbox.java.util.json.', text, flags=re.M) + # annotations + text = re.sub(r'^\s*@(?:jdk\.internal\..*|ValueBased|StableValue).*\n', '', text, flags=re.M) + return text +for name in os.listdir(src): + if not name.endswith('.java') or name == 'StableValue.java': + continue + data = open(os.path.join(src,name),'r').read() + out = xform(data) + target = os.path.join(dst,name) + tmp = target + '.tmp' + open(tmp,'w').write(out) + if os.path.getsize(tmp) == 0: + sys.stderr.write('Refusing to overwrite 0-byte: '+target+'\n'); sys.exit(1) + os.rename(tmp, target) +print('OK') +PY +``` + +## +- MUST: Follow plan → implement → verify. No silent pivots. +- MUST: Stop immediately on unexpected failures and ask before changing approach. +- MUST: Keep edits atomic; avoid leaving mixed partial states. +- SHOULD: Propose options with trade-offs before invasive changes. +- SHOULD: Prefer mechanical, reversible transforms for upstream syncs. +- SHOULD: Validate non-zero outputs before overwriting files. +- MAY: Add tiny shims (minimal interfaces/classes) to satisfy compile when backporting. +- MUST NOT: Commit unverified mass changes; run compile/tests first. +- MUST NOT: Use Perl/sed for multi-line structural edits—prefer Python 3.2 heredoc. + ## Architecture Overview ### Module Structure diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java index 8be48a0..66d4f92 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java @@ -29,15 +29,23 @@ import java.util.List; import jdk.sandbox.java.util.json.JsonArray; import jdk.sandbox.java.util.json.JsonValue; - - -/// JsonArray implementation class -public final class JsonArrayImpl implements JsonArray { +/** + * JsonArray implementation class + */ +public final class JsonArrayImpl implements JsonArray, JsonValueImpl { private final List theValues; + private final int offset; + private final char[] doc; public JsonArrayImpl(List from) { + this(from, -1, null); + } + + public JsonArrayImpl(List from, int o, char[] d) { theValues = from; + offset = o; + doc = d; } @Override @@ -45,6 +53,16 @@ public List values() { return Collections.unmodifiableList(theValues); } + @Override + public char[] doc() { + return doc; + } + + @Override + public int offset() { + return offset; + } + @Override public String toString() { var s = new StringBuilder("["); diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java index cf882ff..476375c 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java @@ -26,18 +26,22 @@ package jdk.sandbox.internal.util.json; import jdk.sandbox.java.util.json.JsonBoolean; - - -/// JsonBoolean implementation class -public final class JsonBooleanImpl implements JsonBoolean { +/** + * JsonBoolean implementation class + */ +public final class JsonBooleanImpl implements JsonBoolean, JsonValueImpl { private final Boolean theBoolean; + private final int offset; + private final char[] doc; - public static final JsonBooleanImpl TRUE = new JsonBooleanImpl(true); - public static final JsonBooleanImpl FALSE = new JsonBooleanImpl(false); + public static final JsonBooleanImpl TRUE = new JsonBooleanImpl(true, null, -1); + public static final JsonBooleanImpl FALSE = new JsonBooleanImpl(false, null, -1); - private JsonBooleanImpl(Boolean bool) { + public JsonBooleanImpl(Boolean bool, char[] doc, int offset) { theBoolean = bool; + this.doc = doc; + this.offset = offset; } @Override @@ -45,6 +49,16 @@ public boolean value() { return theBoolean; } + @Override + public char[] doc() { + return doc; + } + + @Override + public int offset() { + return offset; + } + @Override public String toString() { return String.valueOf(value()); diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java index ab3d308..f8852af 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java @@ -26,16 +26,32 @@ package jdk.sandbox.internal.util.json; import jdk.sandbox.java.util.json.JsonNull; +/** + * JsonNull implementation class + */ +public final class JsonNullImpl implements JsonNull, JsonValueImpl { + private final int offset; + private final char[] doc; -/// JsonNull implementation class -public final class JsonNullImpl implements JsonNull { - - public static final JsonNullImpl NULL = new JsonNullImpl(); + public static final JsonNullImpl NULL = new JsonNullImpl(null, -1); private static final String VALUE = "null"; private static final int HASH = VALUE.hashCode(); - private JsonNullImpl() {} + public JsonNullImpl(char[] doc, int offset) { + this.doc = doc; + this.offset = offset; + } + + @Override + public char[] doc() { + return doc; + } + + @Override + public int offset() { + return offset; + } @Override public String toString() { diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java index f2f5c47..719cb27 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java @@ -29,10 +29,10 @@ import java.math.BigInteger; import java.util.Locale; import jdk.sandbox.java.util.json.JsonNumber; - - -/// JsonNumber implementation class -public final class JsonNumberImpl implements JsonNumber { +/** + * JsonNumber implementation class + */ +public final class JsonNumberImpl implements JsonNumber, JsonValueImpl { private final char[] doc; private final int startOffset; @@ -78,7 +78,7 @@ public Number toNumber() { } else { try { return Long.parseLong(str); - } catch (NumberFormatException ignored) { + } catch(NumberFormatException e) { return new BigInteger(str); } } @@ -97,6 +97,16 @@ public BigDecimal toBigDecimal() { }); } + @Override + public char[] doc() { + return doc; + } + + @Override + public int offset() { + return startOffset; + } + @Override public String toString() { return numString.orElseSet( diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java index a0585ac..5a232b6 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java @@ -29,15 +29,23 @@ import java.util.Map; import jdk.sandbox.java.util.json.JsonObject; import jdk.sandbox.java.util.json.JsonValue; - - -/// JsonObject implementation class -public final class JsonObjectImpl implements JsonObject { +/** + * JsonObject implementation class + */ +public final class JsonObjectImpl implements JsonObject, JsonValueImpl { private final Map theMembers; + private final int offset; + private final char[] doc; public JsonObjectImpl(Map map) { + this(map, -1, null); + } + + public JsonObjectImpl(Map map, int o, char[] d) { theMembers = map; + offset = o; + doc = d; } @Override @@ -45,6 +53,16 @@ public Map members() { return Collections.unmodifiableMap(theMembers); } + @Override + public char[] doc() { + return doc; + } + + @Override + public int offset() { + return offset; + } + @Override public String toString() { var s = new StringBuilder("{"); diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java index 326bb6e..fb9a95c 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java @@ -36,11 +36,13 @@ import jdk.sandbox.java.util.json.JsonString; import jdk.sandbox.java.util.json.JsonValue; -/// Parses a JSON Document char[] into a tree of JsonValues. JsonObject and JsonArray -/// nodes create their data structures which maintain the connection to children. -/// JsonNumber and JsonString contain only a start and end offset, which -/// are used to lazily procure their underlying value/string on demand. Singletons -/// are used for JsonBoolean and JsonNull. +/** + * Parses a JSON Document char[] into a tree of JsonValues. JsonObject and JsonArray + * nodes create their data structures which maintain the connection to children. + * JsonNumber and JsonString contain only a start and end offset, which + * are used to lazily procure their underlying value/string on demand. Singletons + * are used for JsonBoolean and JsonNull. + */ public final class JsonParser { // Access to the underlying JSON contents @@ -100,11 +102,11 @@ private JsonValue parseValue() { * See https://datatracker.ietf.org/doc/html/rfc8259#section-4 */ private JsonObject parseObject() { - offset++; // Walk past the '{' + var startO = offset++; // Walk past the '{' skipWhitespaces(); // Check for empty case if (charEquals('}')) { - return new JsonObjectImpl(Map.of()); + return new JsonObjectImpl(Map.of(), startO, doc); } var members = new LinkedHashMap(); while (hasInput()) { @@ -126,7 +128,7 @@ private JsonObject parseObject() { members.put(name, parseValue()); // Ensure current char is either ',' or '}' if (charEquals('}')) { - return new JsonObjectImpl(members); + return new JsonObjectImpl(members, startO, doc); } else if (charEquals(',')) { skipWhitespaces(); } else { @@ -201,11 +203,11 @@ private String parseName() { * See https://datatracker.ietf.org/doc/html/rfc8259#section-5 */ private JsonArray parseArray() { - offset++; // Walk past the '[' + var startO = offset++; // Walk past the '[' skipWhitespaces(); // Check for empty case if (charEquals(']')) { - return new JsonArrayImpl(List.of()); + return new JsonArrayImpl(List.of(), startO, doc); } var list = new ArrayList(); while (hasInput()) { @@ -213,7 +215,7 @@ private JsonArray parseArray() { list.add(parseValue()); // Ensure current char is either ']' or ',' if (charEquals(']')) { - return new JsonArrayImpl(list); + return new JsonArrayImpl(list, startO, doc); } else if (!charEquals(',')) { break; } @@ -262,7 +264,7 @@ private JsonString parseString() { private JsonBooleanImpl parseTrue() { offset++; if (charEquals('r') && charEquals('u') && charEquals('e')) { - return JsonBooleanImpl.TRUE; + return new JsonBooleanImpl(true, doc, offset); } throw failure(UNEXPECTED_VAL); } @@ -271,7 +273,7 @@ private JsonBooleanImpl parseFalse() { offset++; if (charEquals('a') && charEquals('l') && charEquals('s') && charEquals('e')) { - return JsonBooleanImpl.FALSE; + return new JsonBooleanImpl(false, doc, offset); } throw failure(UNEXPECTED_VAL); } @@ -279,7 +281,7 @@ && charEquals('e')) { private JsonNullImpl parseNull() { offset++; if (charEquals('u') && charEquals('l') && charEquals('l')) { - return JsonNullImpl.NULL; + return new JsonNullImpl(doc, offset); } throw failure(UNEXPECTED_VAL); } @@ -425,11 +427,15 @@ private boolean charEquals(char c) { return false; } + // Return the col position reflective of the current row + private int col() { + return offset - lineStart; + } + private JsonParseException failure(String message) { // Non-revealing message does not produce input source String return new JsonParseException("%s. Location: row %d, col %d." - .formatted(message, line, offset - lineStart), - line, offset - lineStart); + .formatted(message, line, col()), line, col()); } // Parsing error messages ---------------------- diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java index 96c7fd6..f73b899 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java @@ -26,10 +26,10 @@ package jdk.sandbox.internal.util.json; import jdk.sandbox.java.util.json.JsonString; - - -/// JsonString implementation class -public final class JsonStringImpl implements JsonString { +/** + * JsonString implementation class + */ +public final class JsonStringImpl implements JsonString, JsonValueImpl { private final char[] doc; private final int startOffset; @@ -70,6 +70,16 @@ public String value() { return value.orElseSet(this::unescape); } + @Override + public char[] doc() { + return doc; + } + + @Override + public int offset() { + return startOffset; + } + @Override public String toString() { return jsonStr.orElseSet( diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonValueImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonValueImpl.java new file mode 100644 index 0000000..6d595e8 --- /dev/null +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonValueImpl.java @@ -0,0 +1,8 @@ +package jdk.sandbox.internal.util.json; + +// Minimal internal marker for backport compatibility +interface JsonValueImpl { + char[] doc(); + int offset(); +} + diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java index 1d4535a..772d054 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java @@ -28,10 +28,17 @@ import java.util.List; import java.util.Map; import jdk.sandbox.java.util.json.JsonArray; +import jdk.sandbox.java.util.json.JsonAssertionException; +import jdk.sandbox.java.util.json.JsonBoolean; +import jdk.sandbox.java.util.json.JsonNull; +import jdk.sandbox.java.util.json.JsonNumber; import jdk.sandbox.java.util.json.JsonObject; +import jdk.sandbox.java.util.json.JsonString; import jdk.sandbox.java.util.json.JsonValue; -/// Shared utilities for Json classes. +/** + * Shared utilities for Json classes. + */ public class Utils { // Non instantiable @@ -91,4 +98,188 @@ public static String escape(String str) { } return sb == null ? str : sb.toString(); } + + // Use to compose an exception when casting to an incorrect type + public static JsonAssertionException composeTypeError(JsonValue jv, String expected) { + var actual = switch (jv) { + case JsonObject obj -> "JsonObject"; + case JsonArray arr -> "JsonArray"; + case JsonBoolean b -> "JsonBoolean"; + case JsonNull n -> "JsonNull"; + case JsonNumber num -> "JsonNumber"; + case JsonString str -> "JsonString"; + }; + return new JsonAssertionException("%s is not a %s.".formatted(actual, expected) + + ((jv instanceof JsonValueImpl jvi && jvi.doc() != null) ? JsonPath.getPath(jvi) : "")); + } + + public static String getPath(JsonValueImpl jvi) { + return JsonPath.getPath(jvi); + } + + private static final class JsonPath { + + private final int offset; + private final char[] doc; + // Tracked and incremented during path creation + private int row; + private int col; + + private JsonPath(JsonValueImpl jvi) { + this.offset = jvi.offset(); + this.doc = jvi.doc(); + } + + private static String getPath(JsonValueImpl jvi) { + return new JsonPath(jvi).parseToRoot(); + } + + private String parseToRoot() { + var sb = new StringBuilder(); + // Updates the sb + toPath(offset, sb); + // If no new line encountered, col is the starting offset value + if (row == 0) { + col = offset; + } + return " Path: \"%s\". Location: row %d, col %d.".formatted(sb.toString(), row, col); + } + + private void addRow(int curr) { + row++; + if (row == 1) { + col = offset - curr - 1; + } + } + + // Void return type, builds the passed StringBuilder + private void toPath(int offset, StringBuilder sb) { + // Walk past starting char and white space + offset = walkWhitespace(offset - 1); + // If offset is -1, we found the root and are finished + if (offset != -1) { + // Node case + offset = switch (doc[offset]) { + // Does the actual appending + // Walks to the node's starting [ or { + case ',', '[' -> arrayNode(offset, sb); + case ':' -> objectNode(offset, sb); + default -> throw new InternalError(); + }; + toPath(offset, sb); + } + } + + private int walkWhitespace(int offset) { + while (offset >= 0) { + var ws = switch (doc[offset]) { + case ' ', '\t','\r' -> true; + case '\n' -> { + addRow(offset); + yield true; + } + default -> false; + }; + if (!ws) { + break; + } + offset--; + } + return offset; + } + + // Backtracking from an element in a JsonArray either expects a ',' or '[' + // E.g. " [ val ... " or " [ foo, val " + private int arrayNode(int offset, StringBuilder sb) { + int aDepth = 0; + int oDepth = 0; + int values = 0; + boolean inString = false; + while (offset > 0) { + var c = doc[offset]; + if (inString) { + if (c == '"' && doc[offset - 1] != '\\') { + inString = false; + } + } else { + if (c == '[') { + aDepth++; + } else if (c == ']') { + aDepth--; + } else if (c == '{') { + oDepth++; + } else if (c == '}') { + oDepth--; + } else if (c == ',' && aDepth == 0 && oDepth == 0) { + values++; + } else if (c == '"') { + inString = true; + } else if (c == '\n') { + addRow(offset); + } + if (aDepth > 0) { + break; + } + } + offset--; + } + sb.insert(0, '[' + String.valueOf(values)); + return offset; + } + + // Unlike arrayNode, always expects a ':' + // Regardless of value position, always preceded by a member name and colon + private int objectNode(int offset, StringBuilder sb) { + offset--; // Walk past ':' + int depth = 0; + int nameStart = 0; + int nameEnd = 0; + boolean inName = false; + + // Append member name first + while (offset > 0) { + var c = doc[offset]; + if (c == '"' && !inName) { + nameEnd = offset; + inName = true; + } else if (c == '"' && doc[offset - 1] != '\\') { + // Pre-escape check should not throw AIOOBE because guaranteed + // to have enclosing opening bracket + nameStart = offset + 1; + offset--; // Walk past quote + break; + } + offset--; + } + + // Add the name + sb.insert(0, '{' + new String(doc, nameStart, nameEnd - nameStart)); + + boolean inString = false; + // Move to parent offset + while (offset > 0) { + var c = doc[offset]; + if (inString) { + if (c == '"' && doc[offset - 1] != '\\') { + inString = false; + } + } else { + if (c == '{') { + depth++; + } else if (c == '}') { + depth--; + } else if (c == '"') { + inString = true; + } else if (c == '\n') { + addRow(offset); + } + if (depth > 0) { + break; + } + } + offset--; + } + return offset; + } + } } diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonAssertionException.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonAssertionException.java new file mode 100644 index 0000000..82e3f21 --- /dev/null +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonAssertionException.java @@ -0,0 +1,8 @@ +package jdk.sandbox.java.util.json; + +public class JsonAssertionException extends RuntimeException { + public JsonAssertionException(String message) { + super(message); + } +} + From f40eb7d4453aa4bfa6589dd29f03d20870b8fb4d Mon Sep 17 00:00:00 2001 From: Simon Massey <322608+simbo1905@users.noreply.github.com> Date: Fri, 5 Sep 2025 18:06:18 +0100 Subject: [PATCH 3/4] Revise AGENTS.md for clarity and title update Updated the title and improved the structure and clarity of the AGENTS development guide. --- json-java21-schema/AGENTS.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/json-java21-schema/AGENTS.md b/json-java21-schema/AGENTS.md index 2699eec..f08d03e 100644 --- a/json-java21-schema/AGENTS.md +++ b/json-java21-schema/AGENTS.md @@ -1,6 +1,4 @@ -# JSON Schema Validator - Development Guide - -Purpose: Module-level guidance for agents working on `json-java21-schema`. Content is preserved; changes are limited to structure, clarity, and minor wording improvements to align with agents.md best practices. +# JSON Schema Validator - AGENTS Development Guide Note: Prefer mvnd (Maven Daemon) for faster builds. If installed, you can alias mvn to mvnd so top-level instructions work consistently: From a4a8a1298045e92292155415b341d894e9c01059 Mon Sep 17 00:00:00 2001 From: Simon Massey <322608+simbo1905@users.noreply.github.com> Date: Fri, 5 Sep 2025 19:59:34 +0100 Subject: [PATCH 4/4] docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aad331a..e08e6a6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # java.util.json Backport for JDK 21 -Early access to the unstable `java.util.json` API - taken from OpenJDK sandbox July 2025. +Early access to the unstable `java.util.json` API — taken from the OpenJDK jdk-sandbox “json” branch as of 2025-09-04. ## Quick Start @@ -59,7 +59,7 @@ JsonValue backToJson = Json.fromUntyped(Map.of( ## Current Status -This code (as at July 2025) is derived from the official OpenJDK sandbox repository at commit [d22dc2ba89789041c3908cdaafadc1dcf8882ebf](https://github.com/openjdk/jdk-sandbox/commit/d22dc2ba89789041c3908cdaafadc1dcf8882ebf) (Mid July 2025 "Improve hash code spec wording"). +This code (as of 2025-09-04) is derived from the OpenJDK jdk-sandbox repository “json” branch at commit [a8e7de8b49e4e4178eb53c94ead2fa2846c30635](https://github.com/openjdk/jdk-sandbox/commit/a8e7de8b49e4e4178eb53c94ead2fa2846c30635) ("Produce path/col during path building", 2025-08-14 UTC). The original proposal and design rationale can be found in the included PDF: [Towards a JSON API for the JDK.pdf](Towards%20a%20JSON%20API%20for%20the%20JDK.pdf)