The Conformance Suite¶
kPointer ships a language-agnostic conformance suite in the conformance/ directory of the
repository. Each fixture file is a versioned JSON object containing an array of test cases:
{ "schema": "https://kpointer.commonsware.com/conformance/syntax/parsing.schema.json", "version": "0.1.0", "cases": [ … ] }
A conformance runner in any language can deserialize these fixtures and check its own implementation without reading a line of Kotlin.
The reference implementation dogfoods the suite: kpointer-core drives its own tests from the
syntax/* fixtures and kpointer-adapter from the algorithm/* fixtures.
If you are writing a workalike in another language, these fixtures are your acceptance test. This page describes their structure; the workalike guide covers the Kotlin-specific idioms you will need to map.
Directory layout¶
conformance/
manifest.json # Generated inventory of every fixture file below (see "The manifest")
manifest.schema.json
error-codes.json # Machine-readable registry of every canonical KP-XXXX error code
error-codes.schema.json
full-report.schema.json # Schema for the kpointer CLI's `--full-report` JSON output
syntax/
parsing.json # RFC 6901 parsing, fragment, JavaScript access path, auto-dispatch
js-access-path.json # Dedicated JavaScript access path corpus (dot, bracket, escaping)
relative-apply.json # Applying a relative pointer to an absolute pointer
relative-compute.json # Computing the relative pointer between two absolute pointers
algorithm/
resolve.json # Resolving an absolute pointer against a document
mutate.json # Applying set/remove mutations to a document
The manifest¶
manifest.json is a generated, out-of-band integrity index of the suite — a suite version
plus, per fixture file, its version and case count:
{
"manifestFormat": "1.0",
"suiteVersion": "0.1.0",
"files": [
{ "path": "algorithm/mutate.json", "version": "0.1.0", "caseCount": 17 },
{ "path": "algorithm/resolve.json", "version": "0.1.0", "caseCount": 19 }
// …
]
}
It exists to guard against a runner's silent green on nothing failure mode: a runner that globs
fixture files and loads three instead of five cannot detect the two it missed on its own, because a
file it never opened cannot assert anything about itself. The manifest lives outside the glob and
answers "did I see everything I was supposed to?" path is relative to the manifest's own directory
(conformance/), so the manifest stays valid if you vendor the conformance/ tree into your own
repository. files is sorted by path.
It is generated, never hand-authored — the kPointer repository regenerates it with
./gradlew generateConformanceManifest and verifies it with checkConformanceManifest (part of
check), the same generate/verify pattern used for the ABI dumps. If you are writing a runner, treat
it as a read-only guard: after discovering fixture files under a directory, compare your own
{ path → caseCount } inventory against manifest.json's files (when present) and treat any
missing file, unexpected file, or case-count mismatch as a suite-integrity failure distinct from an
ordinary case failure — this is exactly what the kpointer conformance CLI
does, including its distinct exit code. A companion manifest.schema.json documents the envelope
shape for schema-driven deserialization, matching the fixture schemas above.
Normative versus advisory¶
Some expectation fields are normative — every conforming implementation must satisfy them. Others are advisory — they describe kPointer-specific behavior a port may omit if the concept does not apply.
| Field | Where | Normative? |
|---|---|---|
rfc6901 |
parsing, relative-apply |
✅ |
fragment |
parsing |
✅ |
depth, isRoot |
parsing |
✅ |
jsPath |
parsing |
⚠️ advisory — requires the jsPath capability (see Capabilities); absent = skip |
key / index |
relative-apply |
✅ |
element |
resolve |
✅ |
document |
mutate |
✅ |
type: "error" (+ errorCode) |
all | ✅ (see below) |
Error codes¶
Every { "type": "error", … } expectation may carry an errorCode — a KP-XXXX string naming
which specific failure the conforming implementation must raise. errorCode is normative
output: a runner must fail with the named code. How the failure materializes is unconstrained.
When errorCode is absent or null, the fixture accepts any failure.
The full table of error codes — with layer, normative status, and meaning for each — is on the
Error Codes page, and in machine-readable form as conformance/error-codes.json
(schema: error-codes.schema.json). A unit test pins the registry to exactly the set of errorCode
values actually used across syntax/* and algorithm/*, so the two cannot drift apart.
Numeric overflow is deliberately unspecified
The relative-pointer draft declares no maximum for the levels-up prefix or the +N/-N
adjustment, so overflow is port-specific: no error code, no fixture. A port may choose its
own behavior for arbitrarily large numbers.
Nesting depth and pointer length are deliberately unspecified
No maximum nesting depth or pointer segment count is defined. A port may impose its own limit; no error code or fixture covers the case. Very deep documents or very long pointer strings will eventually exhaust stack space on recursive implementations — this is a port-specific concern.
Absent versus error¶
Missing items are treated more gently than are trying to do the impossible:
- A resolution that simply does not find a value returns absent (
null/undefined) — this covers both a missing struct key and an out-of-range list index, whether the miss occurs at the final segment or an intermediate one. Throwing for either is a conformance failure. - Only a structural fault is an error: navigating through a primitive, or addressing a list
with a segment that does not match the RFC 6902
array-indexgrammar (see Coverage matrix below).
The element type system¶
Algorithm-layer fixtures represent document nodes as typed objects rather than raw JSON, because raw
JSON cannot distinguish 42 (integer) from 42.0 (float), nor null-vs-absent. Every element carries
a "type" discriminator:
type |
Additional fields | Description |
|---|---|---|
"string" |
"value": <string> |
A UTF-8 string value |
"boolean" |
"value": <boolean> |
A boolean value |
"long" |
"value": <integer> |
A 64-bit signed integer |
"double" |
"value": <number> |
A 64-bit IEEE 754 floating-point value |
"null" |
(none) | An explicit null value |
"struct" |
"fields": { <key>: <element> } |
An object / map of named elements |
"list" |
"elements": [ <element>, … ] |
An ordered list of elements |
The explicit long-vs-double split lets single-numeric-type languages (JavaScript, AssemblyScript)
preserve and test the semantic distinction.
Two elements match when their types agree and the following per-type rules hold:
- string / boolean / null — value equality.
- long — exact integer equality. Fixture values are bounded to the JS safe-integer range (±9007199254740991 = ±(2⁵³−1)), so every conforming port can round-trip them without precision loss; values outside that range are out of scope and port-specific.
- double — total-ordering equality:
-0.0is not equal to0.0, andNaNis equal toNaN. Ports must use a total-order comparison (e.g.Double.compare/memcmpon the bit pattern), not the language's default==operator, because most languages define-0.0 == 0.0astrue. - struct — same set of keys (order-insensitive) with pairwise-matching values. A port may preserve insertion order internally (the reference does), but key order is not normative and is not tested.
- list — equal length and elementwise matches in order.
Coverage matrix — RFC 6901 edge cases¶
A conformance suite is only as good as the edges it names. The table below maps the classic RFC
6901 edge cases to the fixture names that exercise them, so that a missing row is visibly a
missing fixture.
| Edge case | Example | Fixture(s) |
|---|---|---|
~0 decodes to ~ |
{"a~b":1}, "/a~0b" → 1 |
tilde-zero-escape, resolve-tilde-encoded-key |
~1 decodes to / |
{"a/b":1}, "/a~1b" → 1 |
tilde-one-escape, resolve-tilde-encoded-key |
~01 decodes as ~0+1, yielding key a~1b not a/b |
{"a~1b":1,"a/b":2}, "/a~01b" → 1 |
tilde-zero-one-ordering, resolve-tilde-zero-one-ordering |
~ not followed by 0 or 1 (or trailing ~) is an error |
"/a~2b", "/a~" → error |
error-tilde-invalid-escape, error-tilde-trailing |
"/" is one empty-string segment, not the root |
{"":1}, "/" → 1 (depth 1, not root) |
empty-segment-key |
| Trailing slash produces a second empty-string segment | {"foo":{"":1}}, "/foo/" → 1 |
trailing-slash-two-segments |
double matching uses total ordering: -0.0 ≠ 0.0 |
-0.0 resolves to -0.0, not 0.0 |
resolve-negative-zero-double |
long safe-integer boundary round-trips exactly |
9007199254740991 resolves without precision loss |
resolve-long-safe-integer-max |
| Numeric segment on a list is an integer index | ["a","b"], "/1" → "b" |
resolve-list-element-from-list, resolve-list-element-from-struct |
| Numeric segment on a struct is a literal key, not an index | {"0":"x"}, "/0" → "x" |
resolve-numeric-key-on-struct |
| Non-integer segment on a list is an error, not absent | ["a"], "/foo" → error (not null) |
resolve-list-index-not-integer, error-mutate-list-index-not-integer |
List index must match the RFC 6902 array-index grammar: no leading zero, ASCII digits only |
["a","b"], "/01" → error (not "b") |
resolve-list-index-leading-zero, resolve-list-index-non-ascii-digit, error-mutate-list-index-leading-zero |
| A struct key absent partway through the pointer resolves to absent, like an out-of-range intermediate list index | {"foo":"bar"}, "/missing/deeper" → absent (not error) |
resolve-absent-intermediate-key |
"-" as final segment on a list: appends |
["a"], set "/-" to "b" → ["a","b"] |
append-list-element |
"-" used for removal or at an intermediate position: error |
["a"], remove "/-" → error |
error-mutate-append-token-misused |
"-" as final segment on a struct: literal key, not an error |
{"x":"a"}, set "/-" to "b" → {"x":"a","-":"b"} |
set-dash-key-on-struct |
# query when the resolved pointer is root: error |
base "", relative "0#" → error |
error-hash-at-root |
| Percent-encoded fragment segment | {"föö":1}, "#/f%C3%B6%C3%B6" → 1 |
percent-encoded-fragment |
| Malformed percent-encoding in a fragment: error | "#%zz" → error |
error-fragment-malformed-percent-encoding |
| Percent-encoding across multiple segments | {"föö":{"é":1}}, "#/f%C3%B6%C3%B6/%C3%A9" → 1 |
percent-encoded-fragment-multi-segment |
All edge cases are covered by fixtures.
Writing a runner¶
A minimal runner, per fixture category:
- Deserialize the fixture JSON into typed objects using the fixture schemas.
- Declare which optional features your implementation supports — see Capabilities.
- Build the input (parse the pointer string, build the document from the typed elements, …).
- Execute the operation under test.
- Compare against the
expectfield; report pass/fail/skip using the fixturenameas the test-case id.
The name field is a stable, kebab-case identifier suitable as a test name in any framework.
Next: Writing a Workalike covers the Kotlin idioms you will meet in the reference and how to map them into your target language.
The suite is also published as a versioned, downloadable ZIP — see Distribution — with its own Changelog tracking suite-level changes independently of the library.