Skip to content

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-index grammar (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.
  • doubletotal-ordering equality: -0.0 is not equal to 0.0, and NaN is equal to NaN. Ports must use a total-order comparison (e.g. Double.compare / memcmp on the bit pattern), not the language's default == operator, because most languages define -0.0 == 0.0 as true.
  • 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:

  1. Deserialize the fixture JSON into typed objects using the fixture schemas.
  2. Declare which optional features your implementation supports — see Capabilities.
  3. Build the input (parse the pointer string, build the document from the typed elements, …).
  4. Execute the operation under test.
  5. Compare against the expect field; report pass/fail/skip using the fixture name as 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.