URI references and JSON Pointers

UriReference checks the generic ASCII URI grammar in RFC 3986. It retains scheme, authority, user information, host alternative, exact port token, path, query and fragment spelling. Empty queries and fragments remain distinct from absent ones. Generic URI ports have no assumed transport range. IPv6 and IPvFuture literals are checked; zone identifiers from later URI extensions are outside this grammar.

///|
test {
  let base = @reference.UriReference::parse("HTTPS://Example.test/a/b").unwrap()
  let relative = @reference.UriReference::parse("../c%2fd?#fragment").unwrap()
  let resolved = relative.resolve(base).unwrap()
  assert_eq(resolved.text(), "HTTPS://Example.test/c%2fd?#fragment")
  assert_eq(resolved.identity(), "https://example.test/c%2Fd?#fragment")
  assert_eq(relative.text(), "../c%2fd?#fragment")
  assert_eq(resolved.query(), Some(""))
}

URI resolution follows RFC 3986 section 5.2 and computes an absolute URI; it does not retrieve or inspect a resource. identity() is separate from retained text. It folds scheme and host case, normalizes percent hex and unreserved characters, and removes dot segments from absolute or rooted paths. Relative rootless paths need a base before removing dots. It does not guess default ports, equate HTTP and HTTPS, follow redirects or perform DNS lookup.

Reference adds JSON-target fragment meaning to a URI. Its alternatives are the whole resource, a checked JSON Pointer, or a decoded named identifier. JsonPointer uses plain RFC 6901 escaping; URI percent decoding belongs to the Reference layer. A named identifier remains unresolved until a resource/scope index supplies its target.

///|
test {
  let parts = ["a/b", "~key", "🛰"]
  let pointer = @reference.JsonPointer::from_tokens(parts).unwrap()
  parts.clear()
  assert_eq(pointer.to_string(), "/a~1b/~0key/🛰")
  let reference = @reference.Reference::parse("#/%7E01").unwrap()
  assert_eq(reference.local_pointer().unwrap().tokens(), ["~1"])
  let url = @reference.UriReference::parse("https://example.test/page#/raw~2").unwrap()
  assert_eq(url.fragment(), Some("/raw~2"))
  assert_true(@reference.Reference::from_uri(url) is Err(_))
}

Metadata URLs, editor/dialect identifiers and example externalValue use the general URI type because their fragments need not identify JSON values. OpenRPC and schema $ref values use Reference. Both retain original text for writing. Server URL templates remain uninterpreted strings.

Checked URI/pointer operations accept shared Limits. Defaults allow 8,388,608 UTF-16 source/output units and 100,000 value/token nodes; pointer depth is at most 256 tokens. Resolution bounds both input URIs, merged paths and output before growing their buffers. Pointer lookup inspects immutable children without copying whole array/object views and constructs a diagnostic path only on failure. Raising bounds on a containing document/schema is inherited by its decoders, and typed getters retain values accepted under those chosen bounds.

Explicit resources

A Resource is an immutable JSON value registered under an absolute, fragment-free retrieval URI and optional aliases. The value can be a whole document or an extracted fragment. Aliases name the same resource; they do not add a second retrieval base. schema_roots declares which locations hold draft-07 schemas; nothing else in a generic resource is treated as a schema.

A ResourceSet indexes resources by identity() and has an optional root base URI. Two registrations with the same normalized identity are a ConflictingIdentifier error. Constructors copy their input arrays and getters return copies. Registration counts and the total node count of all values are checked against Limits before the index is built.

///|
test {
  let doc = @json.parse("{\"defs\":{\"a/b\":[1,{\"ok\":true}]}}")
    .unwrap()
    .lower()
    .unwrap()
  let resource = @reference.Resource::new(
    @reference.UriReference::parse("https://example.test/api/doc.json").unwrap(),
    doc,
    aliases=[
      @reference.UriReference::parse("HTTPS://Example.test/api/./doc.json").unwrap(),
    ],
  ).unwrap()
  let set = @reference.ResourceSet::new(
    [resource],
    root_base=Some(
      @reference.UriReference::parse("https://example.test/api/").unwrap(),
    ),
  ).unwrap()
  let found = set.resolve(
    @reference.Reference::parse("doc.json#/defs/a~1b/1").unwrap(),
  )
  assert_true(found.complete())
  let target = found.target().unwrap()
  assert_eq(
    target.resource().unwrap().text(),
    "https://example.test/api/doc.json",
  )
  assert_eq(@json.write(target.target()).unwrap(), "{\"ok\":true}")
  let absent = set.resolve(
    @reference.Reference::parse("file:///etc/passwd").unwrap(),
  )
  assert_false(absent.complete())
  assert_eq(absent.diagnostics()[0].code(), @json.UnavailableResource)
}

ResourceSet::resolve resolves one reference. A relative reference uses the given base, otherwise the root base. Results are a Resolution: the owned target Location (resource URI, pointer and immutable value), the chain of ReferenceStep sites with their absolute URIs, diagnostics, and a complete flag. Missing input gives an incomplete result: MissingBaseUri when there is no absolute base, and UnavailableResource when a resource was not supplied. Broken pointers (UnresolvedReference) and malformed syntax (InvalidReference) are complete failures. A URI scheme never causes an action; file:, javascript: and unknown schemes are only lookup keys.

ResourceSet::follow follows a chain while a caller-supplied function reports that a target is itself a reference. Each later reference resolves against the URI of the resource that holds it. Revisiting a location is a ReferenceCycle. A chain longer than max_depth is incomplete with LimitExceeded. The generic layer never decides by itself that a $ref member is a reference, so $ref inside literal data is not followed. Typed schema and OpenRPC resolution supply that decision.

Draft-07 identifier scopes

The generic resolver reports identifier fragments such as #node as incomplete UnsupportedCheck. SchemaIndex resolves them. It walks the declared schema roots of each resource, plus an optional unnamed input document, through draft-07 subschema positions only. A $id with a URI part names a schema resource at that location and becomes the scope for its subschemas. A $id plain-name fragment names a location within the current scope. Literal and unknown keywords, $ref siblings and declared foreign dialects are not indexed.

///|
test {
  let doc = @json.parse(
      "{\"$id\":\"http://x.test/root.json\",\"definitions\":{\"B\":{\"$id\":\"b.json\",\"definitions\":{\"X\":{\"$id\":\"#x\"}}}}}",
    )
    .unwrap()
    .lower()
    .unwrap()
  let index = @reference.SchemaIndex::new(
    @reference.ResourceSet::empty(),
    input=Some(doc),
    input_roots=[@reference.JsonPointer::parse("").unwrap()],
  )
  let b = index.resolve(
    @reference.Reference::parse("http://x.test/b.json").unwrap(),
  )
  let scope = index.scope(b.target().unwrap())
  assert_eq(scope.unwrap().text(), "http://x.test/b.json")
  for
    text in ["#x", "#/definitions/X", "root.json#/definitions/B/definitions/X"] {
    let found = index.resolve(
      @reference.Reference::parse(text).unwrap(),
      base=scope,
    )
    assert_eq(
      found.target().unwrap().identity(),
      "#/definitions/B/definitions/X",
    )
  }
}

Every access path resolves to the same owned Location: the physical resource (None for the unnamed input) and the pointer within it. Resolving never copies or expands a schema. An identifier that claims a location already named by another identifier or registered resource is a ConflictingIdentifier; the first binding is kept. A relative $id without a base is MissingBaseUri. A non-plain-name fragment such as #/a, which draft-07 leaves undefined, is kept and reported as incomplete UnsupportedCheck. In an unnamed input with no base, a relative $id names its own location: fragment-only references inside it resolve there, and references that change the path report MissingBaseUri. Schemas reached by $ref are indexed too, even outside the declared roots, and nested roots are walked after their ancestors. Plain names are keyed by the location that sets their scope, so retrieval URIs, aliases and $id spellings of a document all reach them. Typed OpenRPC targets are resolved by the root package's document validation. This development module is not a qualified release candidate.

View source on GitHub