feat(swift_core): implement SchemaCompiler for AST translation#1736
feat(swift_core): implement SchemaCompiler for AST translation#1736pinieb wants to merge 33 commits into
Conversation
This change establishes the foundational infrastructure for Swift libraries in the A2UI monorepo. There is just enough code (JSONValue.swift and its tests) to standup a SwiftPM library and get CI up and running.
Every node in a compiled JSON Schema AST must have a deterministic identity to allow for $ref resolution and accurate error reporting. This identity is a combination of a Base URI and a JSON Pointer (RFC 6901). This change introduces the mechanisms to do this.
…xt management - Add `ValidationContext` and `ValidatorConfiguration` to safely pass execution state (like max depth limits) down the AST without mutation. - Add `ValidationResult` struct designed to support the 2020-12 hierarchical output format, tracking errors and annotations. - Define the `KeywordEvaluator` protocol to standardize keyword execution. - Implement the `SchemaNode` executable AST struct with logic to aggregate results, errors, and annotations from multiple evaluators. - Add test coverage for recursion depth limits and result aggregation.
…egistry - Add `SchemaRegistry` as a shared reference type to cache compiled `SchemaNode`s, enabling synchronous `$ref` resolution and preventing infinite CoW loops during deep evaluation. - Add `KeywordRegistry` and `EvaluatorFactory` to decouple keyword parsing from the core engine, allowing runtime extensibility. - Stub `SchemaCompiler` to resolve dependency graph for applicators. - Add test coverage for node caching and custom keyword injection.
- Fully implement `SchemaCompiler` to translate `JSONValue` into `SchemaNode`s. - Add support for boolean schemas (`true` = always valid, `false` = always invalid). - Implement structural keyword interception (`$id`, `$defs`, `$dynamicAnchor`). - Ensure `$id` properly updates the base URI and resets the JSON pointer scope. - Ensure `$defs` are recursively compiled and cached in the `SchemaRegistry` for synchronous `$ref` lookup during evaluation. - Add comprehensive compiler test coverage for scope resets and definition caching.
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Pete Biencourt <pinieb@users.noreply.github.com>
…eb/a2ui into swift-json-schema-protocols
# Conflicts: # Package.swift # renderers/swift_core/AGENTS.md # swift/core/CODING_STANDARDS.md # swift/core/JSONSchema/README.md # swift/core/JSONSchema/Sources/JSONPointer.swift # swift/core/JSONSchema/Sources/JSONValue.swift # swift/core/JSONSchema/Sources/KeywordEvaluator.swift # swift/core/JSONSchema/Sources/SchemaIdentity.swift # swift/core/JSONSchema/Sources/SchemaNode.swift # swift/core/JSONSchema/Sources/SchemaRegistry.swift # swift/core/JSONSchema/Sources/ValidationContext.swift # swift/core/JSONSchema/Sources/ValidationError.swift # swift/core/JSONSchema/Sources/ValidationResult.swift # swift/core/JSONSchema/Sources/ValidatorConfiguration.swift # swift/core/JSONSchema/Tests/ExecutionContractsTests.swift # swift/core/JSONSchema/Tests/JSONValueTests.swift # swift/core/JSONSchema/Tests/SchemaIdentityTests.swift # swift/core/README.md
…gement # Conflicts: # swift/core/JSONSchema/Sources/KeywordRegistry.swift # swift/core/JSONSchema/Sources/SchemaCompiler.swift # swift/core/JSONSchema/Tests/KeywordRegistryTests.swift # swift/core/JSONSchema/Tests/SchemaRegistryTests.swift
…piler # Conflicts: # swift/core/JSONSchema/Sources/SchemaCompilerError.swift # swift/core/JSONSchema/Tests/SchemaCompilerTests.swift
…and compiler tests
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Signed-off-by: Pete Biencourt <pinieb@users.noreply.github.com>
There was a problem hiding this comment.
Code Review
This pull request introduces the A2UISwiftCore package, establishing a generic JSON Schema Draft 2020-12 validator and DSL builder target (JSONSchema) in Swift, complete with unit tests, documentation, and formatting scripts. The review feedback highlights critical improvements needed for full schema specification compliance: SchemaIdentity should support plain string anchors in URI fragments rather than assuming they are always JSONPointers, and compiled schemas must be registered under their anchor-based URIs in the SchemaRegistry to prevent reference resolution failures. Additionally, updatingBaseURI should be updated to correctly handle and split fragments in the new URI to avoid malformed URIs.
| guard let parsedPointer = JSONPointer(stringRepresentation: fragment) else { | ||
| return nil | ||
| } |
There was a problem hiding this comment.
The SchemaIdentity initializer currently assumes that any URI fragment is a valid JSONPointer. If the fragment is a plain anchor (e.g., #my-anchor), JSONPointer(stringRepresentation:) will return nil, causing the entire SchemaIdentity initialization to fail.\n\nIn JSON Schema Draft 2020-12, plain anchors are valid URI fragments used by $anchor and $dynamicAnchor. To support resolving schemas via anchors, SchemaIdentity should be updated to allow fragments that are either a JSONPointer or a plain string anchor.
| var parsedAnchor: String? = nil | ||
| if let anchorVal = dict["$dynamicAnchor"], case .string(let anchorStr) = anchorVal { | ||
| parsedAnchor = anchorStr | ||
| } |
There was a problem hiding this comment.
While $dynamicAnchor is parsed here, the compiled SchemaNode is not registered in the SchemaRegistry under its anchor-based URI (e.g., baseURI + \"#\" + anchorStr). Additionally, interception and registration for the standard $anchor keyword are completely missing.\n\nThis is likely a consequence of SchemaIdentity only supporting JSONPointer fragments. Without registering schemas under their anchor URIs, resolving references ($ref or $dynamicRef) that point to anchors (e.g., \"$ref\": \"#my-anchor\") will fail during evaluation.
| public func updatingBaseURI(to newURI: String) -> SchemaIdentity { | ||
| return SchemaIdentity(uncheckedBaseURI: newURI, pointer: JSONPointer()) | ||
| } |
There was a problem hiding this comment.
If newURI contains a fragment, SchemaIdentity(uncheckedBaseURI:pointer:) will not split it, leaving the fragment inside baseURI. This can lead to malformed URIs when subsequent paths are appended (e.g., https://example.com/schema#foo#/properties/bar).\n\nWe can resolve this by attempting to parse newURI using SchemaIdentity(uri:) first, which correctly splits the fragment if present.
public func updatingBaseURI(to newURI: String) -> SchemaIdentity {
if let parsed = SchemaIdentity(uri: newURI) {
return parsed
}
return SchemaIdentity(uncheckedBaseURI: newURI, pointer: JSONPointer())
}…eb/a2ui into swift-json-schema-protocols
| return .success() | ||
| } | ||
|
|
||
| if text.count >= minLength { |
There was a problem hiding this comment.
This example uses text.count to validate the length of a string.
In Swift, text.count returns the number of Characters (extended grapheme clusters), which may not match the number of Unicode code points. The JSON Schema spec says: "The length of a string instance is defined as the number of its Unicode codepoints." For example, the emoji "👨👩👧👦" is a single extended grapheme cluster (text.count == 1), but consists of 7 Unicode code points. Under JSON Schema rules, its length is 7.
To be spec-compliant, string length validators should use text.unicodeScalars.count.
| let node = SchemaNode( | ||
| identity: currentIdentity, | ||
| dynamicAnchor: parsedAnchor, | ||
| evaluators: evaluators | ||
| ) | ||
| schemaRegistry.register(node, for: currentIdentity) |
There was a problem hiding this comment.
When a subschema defines an $id keyword, currentIdentity is updated to the new base URI, and the compiled SchemaNode is registered under that new URI. However, according to the JSON Schema specification, the schema must also remain resolvable via its original lexical path (the identity parameter passed into compile()). If you do not register it under the lexical URI, any local $ref pointer referencing this schema by its document path (e.g. #/properties/child or #/$defs/mySchema) will fail to resolve during evaluation.
| guard let basePointer = JSONPointer(stringRepresentation: fragment) else { | ||
| return nil |
There was a problem hiding this comment.
In this code, if the baseURI contains a fragment (e.g. https://example.com/schema#invalid-fragment), you attempt to parse it as a JSONPointer. If parsing fails (since it doesn't start with /), the initializer silently returns nil at line 52.
When called during compilation, this could cause the compiler to fail with a generic error or crash due to forced unwrapping of SchemaIdentity elsewhere.
Consider adding a specific compiler warning or throwing a descriptive error when a base URI with an invalid fragment is encountered, rather than silently failing initialization.
|
I think I have the same comment as Jacob did on your other PR: is there a standard Swift library for this? It would be good not to have to maintain a JSON Schema validator if there's a well maintained one out there already. |
Description
SchemaCompilerto translateJSONValueintoSchemaNodes.true= always valid,false= always invalid).$id,$defs,$dynamicAnchor).$idproperly updates the base URI and resets the JSON pointer scope.$defsare recursively compiled and cached in theSchemaRegistryfor synchronous
$reflookup during evaluation.Pre-launch Checklist
For this PR:
run_tests.shfor SwiftIf you need help, consider asking for advice on the discussion board.