referencing

    A faithful MoonBit port of python-jsonschema/referencing: cross-specification JSON referencing.

    json
    jsonschema
    referencing
    uri
    Download zip
    Author
    Version
    0.1.0
    License
    MIT
    Last updated
    yesterday
    Downloads
    5

    #bobzhang/referencing

    A faithful MoonBit port of python-jsonschema/referencing: cross-specification, implementation-agnostic JSON referencing — the engine behind $ref, $dynamicRef and $recursiveRef resolution in python-jsonschema.

    Packages:

    packageupstreamcontents
    bobzhang/referencingreferencing, referencing.exceptions, referencing.retrieval (and the definitions behind referencing.jsonschema)Specification, Resource, Registry, Resolver, Resolved, Retrieved, Anchor, ReferencingError, to_cached_resource
    bobzhang/referencing/jsonschemareferencing.jsonschemadraft202012() … draft3(), specification_with, dynamic_anchor, lookup_recursive_ref, empty_registry, the $id/anchor rules (re-exported from the root)
    bobzhang/referencing/urlliburllib.parseurljoin, urlsplit, urlparse, urlunsplit, urlunparse, urldefrag, unquote — exact ports, tested against CPython

    #Registries and resolvers

    Resources are Json documents paired with a Specification. Registries are immutable (backed by persistent hash maps): every "modification" returns a new registry, and subresources are discovered lazily ("crawled") on demand.

    ///|
    test "resolve a reference" {
    let schema : Json = {
    "$id": "https://example.com/person",
    "$defs": { "name": { "$anchor": "name", "type": "string" } },
    }
    let resource = @referencing.draft202012().create_resource(schema)
    let registry = @referencing.Registry::new().with_resource(
    "https://example.com/person", resource,
    )
    let resolver = registry.resolver()
    // by URI + JSON pointer
    let resolved = resolver.lookup("https://example.com/person#/$defs/name")
    json_inspect(resolved.contents, content={
    "$anchor": "name",
    "type": "string",
    })
    // by plain-name anchor
    let by_anchor = resolver.lookup("https://example.com/person#name")
    assert_true(by_anchor.contents == resolved.contents)
    // `resolved.resolver` carries the new base URI (and dynamic scope) for
    // resolving references found *inside* the resolved contents
    inspect(resolved.resolver.base_uri(), content="https://example.com/person")
    }

    Resource::from_contents detects the specification from $schema; with a default_specification it falls back to it instead of raising:

    ///|
    test "detect specifications" {
    let resource = @referencing.Resource::from_contents({
    "$schema": "http://json-schema.org/draft-07/schema#",
    })
    inspect(resource.specification, content="<Specification name='draft-07'>")
    let fallback = @referencing.Resource::from_contents(
    { "type": "integer" },
    default_specification=@referencing.draft202012(),
    )
    inspect(fallback.specification, content="<Specification name='draft2020-12'>")
    }

    #Errors

    All errors are constructors of the ReferencingError suberror, and their Show output is exactly Python's str(error). Resolver::lookup raises a generic Error (custom callbacks may raise anything); use @referencing.is_unresolvable(error) for Python's except referencing.exceptions.Unresolvable (which also catches PointerToNowhere, NoSuchAnchor and InvalidAnchor), and ReferencingError::from_error to get at the details.

    ///|
    test "unresolvable references" {
    let resolver = @referencing.Registry::new().resolver_with_root(
    @referencing.Resource::new_opaque({ "foo": {} }),
    )
    try resolver.lookup("#/foo/bar") catch {
    error => {
    assert_true(@referencing.is_unresolvable(error))
    inspect(error, content="'/foo/bar' does not exist within {'foo': {}}")
    }
    } noraise {
    _ => fail("expected an error")
    }
    }

    #Retrieval

    A registry can be given a retrieve closure, called for unknown URIs; to_cached_resource turns a "URI to serialized JSON" function into a caching retriever:

    ///|
    test "retrieval" {
    let retrieve = @referencing.to_cached_resource(uri => {
    guard uri == "urn:example:positive" else {
    raise @referencing.NoSuchResource(reference=uri)
    }
    "{\"$schema\": \"https://json-schema.org/draft/2020-12/schema\", \"minimum\": 0}"
    })
    let resolver = @referencing.Registry::new(retrieve~).resolver()
    let resolved = resolver.lookup("urn:example:positive")
    json_inspect(resolved.contents, content={
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "minimum": 0,
    })
    // the registry inside the returned resolver now contains the resource
    assert_true(resolved.resolver.registry().contains("urn:example:positive"))
    }

    #Dynamic and recursive references

    ///|
    test "dynamic scope" {
    let root = @referencing.draft202012().create_resource({
    "$id": "https://example.com/tree",
    "$dynamicAnchor": "node",
    "$defs": { "strict": { "$id": "strict", "$dynamicAnchor": "node" } },
    })
    let resolver = @referencing.Registry::new()
    .with_resource("https://example.com/tree", root)
    .resolver()
    let tree = resolver.lookup("https://example.com/tree")
    let strict = tree.resolver.lookup("strict")
    let scope = strict.resolver.dynamic_scope().map(pair => pair.0).to_array()
    assert_eq(scope, ["https://example.com/tree"])
    // `#node` is a dynamic anchor: the outermost one in the dynamic scope wins
    let node = strict.resolver.lookup("#node")
    assert_true(node.contents == root.contents)
    }

    @referencing.lookup_recursive_ref(resolver) implements draft 2019-09's $recursiveRef: "#".

    #Differences from upstream

    • Errors. Python's exception classes are constructors of one ReferencingError suberror (plus UnknownDialect, which upstream defines in referencing.jsonschema). The ref attribute is named reference (ref is a reserved word in MoonBit). Unretrievable carries the retriever's error as cause (Python's __cause__). Upstream's exception subclassing is replaced by ReferencingError::is_unresolvable / is_unresolvable. Where Python raises a builtin ValueError (malformed IPv6 URLs in urlsplit/urljoin, conflicting retrievers in Registry::combine), we raise @urllib.ValueError.
    • Naming. Specification.OPAQUE is opaque_specification and Resource.opaque is Resource::new_opaque (opaque is reserved). registry[uri] is Registry::at; resource @ registry is Registry::with_identified_resource(s); Specification.detect (class or instance method) is Specification::detect(contents, default?). DRAFT202012 etc. are functions (draft202012(), always returning the same object) because their callbacks refer back to specification_with, which MoonBit rejects as a cycle among top-level values.
    • Package layout. The JSON Schema specifications are defined in the root package (Specification::detect needs them and packages cannot be mutually dependent) and re-exported by bobzhang/referencing/jsonschema.
    • Anchors. The referencing.typing.Anchor protocol is a single Anchor struct with a kind tag ("Anchor", "DynamicAnchor", or custom via Anchor::custom) and an optional resolve closure; DynamicAnchor(...) is dynamic_anchor(...) and isinstance(a, DynamicAnchor) is a.is_dynamic().
    • Ill-typed documents. Where upstream would crash with a Python TypeError/AttributeError/ValueError on ill-typed JSON (e.g. a numeric $id, properties that is not an object, a non-integer array index in a JSON pointer, a pointer through a number), we ignore the keyword or raise PointerToNowhere respectively. A non-string $schema with a default specification falls back to the default. Python's int() accepts Unicode digits in array indices; we accept ASCII digits only.
    • Equality. Callbacks (Specification fields, retrieve) compare by identity, as in Python; Specifications compare by identity or by name and identical callbacks.
    • Iteration order. Sets of keywords are iterated in the order upstream lists them (Python's set order is unspecified); JSON objects in insertion order; registries in (unspecified) hash order.
    • retrieval.to_cached_resource takes the retrieve function as its first argument (instead of being a decorator factory) and is specialized to String documents; caches are unbounded_cache() (default) or lru_cache(maxsize=...). As with functools.lru_cache, errors are not cached.
    • urllib. urlsplit skips Python's NFKC check of non-ASCII netlocs (_checknetloc). py_repr of floats in error messages approximates Python's repr.
    • Suite. All 296 referencing-suite files pass, except the 12 rfc3986-normalization-* files which upstream also marks as expected failures ("APIs need to change for proper URL support"); the generated tests assert that they still fail.

    RetrieveCache

    type RetrieveCache = ((String) -> Resource raise) -> ((String) -> Resource raise)

    A caching strategy for retrieval functions (Python passes e.g. functools.lru_cache(maxsize=...) as cache): it wraps a retrieve function into a caching one.

    ReferencingError

    pub(all) suberror ReferencingError {
    NoSuchResource(reference~ : String)
    NoInternalID(resource~ : Resource)
    Unretrievable(reference~ : String, cause~ : Error?)
    CannotDetermineSpecification(contents~ : Json)
    Unresolvable(reference~ : String)
    PointerToNowhere(reference~ : String, resource~ : Resource)
    NoSuchAnchor(reference~ : String, resource~ : Resource, anchor~ : String)
    InvalidAnchor(reference~ : String, resource~ : Resource, anchor~ : String)
    UnknownDialect(uri~ : String)
    }

    All errors raised by this library (Python's referencing.exceptions, plus referencing.jsonschema.UnknownDialect).

    Python models these as an exception class hierarchy where PointerToNowhere, NoSuchAnchor and InvalidAnchor are subclasses of Unresolvable; use ReferencingError::is_unresolvable to test for "an Unresolvable or any subclass thereof".

    Show renders the same text as Python's str(error).

    ReferencingError::from_error

    fn ReferencingError::from_error(error : Error) -> ReferencingError?

    Recover a ReferencingError from a generic Error (as raised by e.g. Resolver::lookup), if it is one.

    ReferencingError::is_unresolvable

    fn ReferencingError::is_unresolvable(self : ReferencingError) -> Bool

    Whether this error is an Unresolvable or one of its "subclasses" (PointerToNowhere, NoSuchAnchor, InvalidAnchor), i.e. whether Python's except referencing.exceptions.Unresolvable would catch it.

    ReferencingError::reference

    fn ReferencingError::reference(self : ReferencingError) -> String?

    The ref carried by the error, if it has one.

    Anchor

    pub struct Anchor {
    name : String
    resource : Resource
    // private fields
    }

    An anchor within a Resource (Python's referencing.Anchor, and more generally the referencing.typing.Anchor protocol).

    Plain anchors resolve to their resource's contents. Other kinds of anchors (such as JSON Schema 2020-12's dynamic anchors, see dynamic_anchor) carry a kind tag (Python's class name) and their own resolution closure.
    impl Eq for Anchor
    impl Show for Anchor

    Anchor::custom

    fn Anchor::custom(name~ : String, resource~ : Resource, kind~ : String, resolve~ : (Anchor, Resolver) -> Resolved raise) -> Anchor

    Create a custom kind of anchor (Python: any object implementing the referencing.typing.Anchor protocol). kind plays the role of the Python class name (used for equality and display), and resolve is called with the anchor itself and the resolver.

    Anchor::is_dynamic

    fn Anchor::is_dynamic(self : Anchor) -> Bool

    Whether this anchor is a DynamicAnchor (Python's isinstance(anchor, DynamicAnchor)).

    Anchor::kind

    fn Anchor::kind(self : Anchor) -> String

    The kind of the anchor: "Anchor" for simple anchors, "DynamicAnchor" for JSON Schema dynamic anchors, or a custom kind.

    Anchor::new

    fn Anchor::new(name~ : String, resource~ : Resource) -> Anchor

    Create a simple anchor in a Resource.

    Anchor::resolve

    fn Anchor::resolve(self : Anchor, resolver : Resolver) -> Resolved raise

    Return the resource for this anchor.

    Registry

    pub struct Registry {
    // private fields
    }

    A registry of Resources, each identified by their canonical URIs.

    Registries store a collection of in-memory resources, and optionally enable additional resources which may be stored elsewhere (e.g. in a database, a separate set of files, over the network, etc.).

    They also lazily walk their known resources, looking for subresources within them. In other words, subresources contained within any added resources will be retrievable via their own IDs (though this discovery of subresources will be delayed until necessary).

    Registries are immutable (backed by persistent hash maps, so copies are cheap), and their methods return new instances of the registry with the additional resources added to them.

    The retrieve closure can be used to configure retrieval of resources dynamically, either over the network, from a database, or the like. It is called if any URI not present in the registry is accessed. It must either return a Resource or else raise NoSuchResource indicating that the resource does not exist even according to the retrieval logic; any other error is wrapped in Unretrievable.
    impl Eq for Registry
    impl Show for Registry

    Registry::anchor

    fn Registry::anchor(self : Registry, uri : String, name : String) -> Retrieved[Anchor] raise

    Retrieve a given anchor from a resource which must already be crawled.

    Raises NoSuchResource if the resource is unknown, InvalidAnchor if the name contains a / (and so could never be a plain-name anchor), and NoSuchAnchor otherwise if the anchor is missing.

    Registry::at

    fn Registry::at(self : Registry, uri : String) -> Resource raise ReferencingError

    Return the (already crawled) Resource identified by the given URI (Python's registry[uri]). Trailing #s are ignored.

    Raises NoSuchResource if it is not present.

    Registry::combine

    Combine together one or more other registries, producing a unified one.

    Later registries' resources take precedence. Raises ValueError if two registries have conflicting (non-default) retrieval functions; retrieval functions are compared by identity.

    Registry::contains

    fn Registry::contains(self : Registry, uri : String) -> Bool

    Whether a (crawled) resource is identified by the given URI (Python's uri in registry).

    Registry::contents

    fn Registry::contents(self : Registry, uri : String) -> Json raise ReferencingError

    Retrieve the (already crawled) contents identified by the given URI.

    Registry::crawl

    Crawl all added resources, discovering subresources (and anchors).

    Raises only if joining an identifier onto its base URI fails (@urllib.ValueError, e.g. for malformed IPv6 hosts).

    Registry::get

    fn Registry::get(self : Registry, uri : String) -> Resource?

    Return the (already crawled) Resource identified by the given URI, if any (Python's registry.get(uri)).

    Registry::get_or_retrieve

    fn Registry::get_or_retrieve(self : Registry, uri : String) -> Retrieved[Resource] raise

    Get a resource from the registry, crawling or retrieving if necessary.

    May involve crawling to find the given URI if it is not already known, so the returned object contains both the resource as well as the registry which ultimately contained it (including any newly retrieved resource).

    Registry::is_empty

    fn Registry::is_empty(self : Registry) -> Bool

    Whether the registry has no resources (Python's not registry).

    Registry::iter

    fn Registry::iter(self : Registry) -> Iter[String]

    Iterate over all crawled URIs in the registry (in unspecified order).

    Registry::iter2

    fn Registry::iter2(self : Registry) -> Iter2[String, Resource]

    Iterate over all crawled (uri, resource) pairs (Python's .items()).

    Registry::keys

    fn Registry::keys(self : Registry) -> Iter[String]

    All crawled URIs in the registry.

    Registry::length

    fn Registry::length(self : Registry) -> Int

    Count the total number of fully crawled resources in this registry.

    Registry::new

    fn Registry::new(resources? : ArrayView[(String, Resource)], anchors? : ArrayView[((String, String), Anchor)], retrieve? : (String) -> Resource raise) -> Registry

    Create a registry (Python's Registry(resources, anchors=..., retrieve=...)).

    Note that, exactly as upstream, resources passed here are considered already crawled (they are not walked for subresources); use Registry::with_resources to add resources which should be crawled. Without retrieve, unknown URIs raise NoSuchResource.

    Registry::remove

    fn Registry::remove(self : Registry, uri : String) -> Registry raise ReferencingError

    Return a registry with the resource identified by a given URI removed.

    Raises NoSuchResource if it is not present.

    Registry::resolver

    fn Registry::resolver(self : Registry, base_uri? : String) -> Resolver

    Return a Resolver which resolves references against this registry.

    Registry::resolver_with_root

    fn Registry::resolver_with_root(self : Registry, resource : Resource) -> Resolver

    Return a Resolver with a specific root resource, which is added to the registry under its ID (or the empty URI if it has none).

    Registry::uncrawled_count

    fn Registry::uncrawled_count(self : Registry) -> Int

    The number of added resources not yet crawled.

    Registry::with_contents

    fn Registry::with_contents(self : Registry, pairs : ArrayView[(String, Json)], default_specification? : Specification) -> Registry raise ReferencingError

    Add the given contents to the registry, autodetecting when necessary (see Resource::from_contents).

    Registry::with_identified_resource

    fn Registry::with_identified_resource(self : Registry, resource : Resource) -> Registry raise ReferencingError

    Create a new registry with the resource added using its internal ID (Python's resource @ registry).

    Registry::with_identified_resources

    fn Registry::with_identified_resources(self : Registry, new : ArrayView[Resource]) -> Registry raise ReferencingError

    Create a new registry with resources added using their internal IDs (Python's [resources...] @ registry).

    Raises NoInternalID if any resource has no internal ID (e.g. the $id keyword in modern JSON Schema versions).

    Registry::with_resource

    fn Registry::with_resource(self : Registry, uri : String, resource : Resource) -> Registry

    Add the given Resource to the registry, without crawling it.

    Trailing #s are stripped from the URI (empty fragment URIs are equivalent to URIs without the fragment).

    Registry::with_resources

    fn Registry::with_resources(self : Registry, pairs : ArrayView[(String, Resource)]) -> Registry

    Add the given Resources to the registry, without crawling them.

    Resolved

    pub(all) struct Resolved {
    contents : Json
    resolver : Resolver
    }

    A reference resolved to its contents by a Resolver, along with the resolver to use for resolving any references within those contents (which carries the new base URI, the updated registry and the dynamic scope).

    Resolver

    pub struct Resolver {
    // private fields
    }

    A reference resolver.

    Resolvers help resolve references (including relative ones) by pairing a fixed base URI with a Registry.

    This object, under normal circumstances, is expected to be used by implementers of libraries built on top of referencing (e.g. JSON Schema implementations or other libraries resolving JSON references), not directly by end-users populating registries or while writing schemas or other resources.

    References are resolved against the base URI, and the combined URI is then looked up within the registry.

    The process of resolving a reference may itself involve calculating a new base URI for future reference resolution (e.g. if an intermediate resource sets a new base URI), or may involve encountering additional subresources and adding them to a new registry.
    impl Show for Resolver

    Resolver::base_uri

    fn Resolver::base_uri(self : Resolver) -> String

    The base URI against which references are resolved.

    Resolver::dynamic_scope

    fn Resolver::dynamic_scope(self : Resolver) -> Iter[(String, Registry)]

    In specs with such a notion, return the URIs in the dynamic scope (most recent first), each paired with this resolver's registry.

    Resolver::in_subresource

    fn Resolver::in_subresource(self : Resolver, subresource : Resource) -> Resolver raise
    ValueError

    Create a resolver for a subresource (which may have a new base URI).

    Returns this very resolver (physically) if the subresource has no ID.

    Resolver::lookup

    fn Resolver::lookup(self : Resolver, reference : String) -> Resolved raise

    Resolve the given reference to the resource it points to.

    Raises (as ReferencingErrors):

    • Unresolvable (or PointerToNowhere, NoSuchAnchor, InvalidAnchor, see ReferencingError::is_unresolvable) if the reference isn't resolvable: NoSuchAnchor if the reference is to a URI where a resource exists but contains a plain name fragment which does not exist within the resource, PointerToNowhere if the reference is to a URI where a resource exists but contains a JSON pointer to a location within the resource that does not exist.
    • CannotDetermineSpecification if a retrieved resource raised it.

    It may also raise @urllib.ValueError for malformed URIs, or any error raised by custom specification / anchor callbacks.

    Resolver::new

    fn Resolver::new(base_uri~ : String, registry~ : Registry, previous? :
    List
    [String]) -> Resolver

    Create a resolver (Python's Resolver(base_uri=..., registry=...)). Prefer Registry::resolver.

    Resolver::previous

    fn Resolver::previous(self : Resolver) ->
    List
    [String]

    The previous base URIs (most recent first), i.e. the URIs in the dynamic scope (Python's _previous).

    Resolver::registry

    fn Resolver::registry(self : Resolver) -> Registry

    The registry references are looked up in.

    Resource

    pub struct Resource {
    contents : Json
    specification : Specification
    }

    A document (deserialized JSON) with a concrete interpretation under a specification.

    In other words, a Json value along with a Specification which describes how the document interacts with referencing -- both internally (how it refers to other resources) and externally (how it should be identified such that it is referenceable by other documents).
    impl Eq for Resource
    impl Show for Resource

    Resource::anchors

    fn Resource::anchors(self : Resource) -> Array[Anchor]

    Retrieve this resource's (specification-specific) anchors.

    Resource::from_contents

    fn Resource::from_contents(contents : Json, default_specification? : Specification) -> Resource raise ReferencingError

    Create a resource guessing which specification applies to the contents.

    Without default_specification, raises CannotDetermineSpecification if the contents have no discernible information (a $schema keyword) which could be used to guess which specification they identify as (and UnknownDialect for unknown $schemas). See Specification::detect.

    Resource::id

    fn Resource::id(self : Resource) -> String?

    Retrieve this resource's (specification-specific) identifier, with any trailing # characters stripped.

    Resource::new

    fn Resource::new(contents : Json, specification~ : Specification) -> Resource

    Create a resource with an explicit specification (Python's Resource(contents=..., specification=...)).

    Resource::new_opaque

    fn Resource::new_opaque(contents : Json) -> Resource

    Create an opaque Resource -- i.e. one with the opaque specification.

    Resource::pointer

    fn Resource::pointer(self : Resource, pointer : String, resolver : Resolver) -> Resolved raise

    Resolve the given JSON pointer (the fragment of a reference, e.g. /definitions/foo) within this resource.

    As upstream, the pointer (minus its first character) is first percent-decoded with @urllib.unquote, then split on /; each segment indexing into an object has ~1 replaced by / and then ~0 by ~, while segments indexing into arrays (and strings) are parsed as Python integers (so negative indices count from the end, as in Python).

    Raises PointerToNowhere if the pointer points to a location not present in the document.

    Resource::subresources

    fn Resource::subresources(self : Resource) -> Array[Resource]

    Retrieve this resource's subresources. Each is interpreted by detecting its specification, defaulting to this resource's specification.

    Retrieved

    pub(all) struct Retrieved[T] {
    value : T
    registry : Registry
    }

    A value retrieved from a Registry, along with the registry which ultimately contained it (which may have been crawled or have had a resource retrieved into it).

    Segment

    pub(all) enum Segment {
    Key(String)
    Index(Int)
    } derive(Eq,
    Debug
    )

    A segment of a JSON pointer as seen by Specification::maybe_in_subresource (Python's int | str): array indices are Index, everything else Key.

    Segment::is_key

    fn Segment::is_key(self : Segment, key : String) -> Bool

    Whether the segment is the string key (Python's segment == key).

    Specification

    pub struct Specification {
    name : String
    id_of : (Json) -> String?
    subresources_of : (Json) -> Array[Json]
    maybe_in_subresource : (Array[Segment], Resolver, Resource) -> Resolver raise
    anchors_in : (Specification, Json) -> Array[Anchor]
    }

    A specification which defines referencing behavior.

    The various callbacks of a Specification allow for varying referencing behavior across JSON Schema specification versions, etc. Each callback is a first-class closure stored in a field, and also callable with method syntax (spec.id_of(contents)).
    impl Eq for Specification

    Specification::anchors_in

    fn Specification::anchors_in(self : Specification, contents : Json) -> Array[Anchor]

    Retrieve the anchors contained in the given document.

    Specification::create_resource

    fn Specification::create_resource(self : Specification, contents : Json) -> Resource

    Create a resource which is interpreted using this specification.

    Specification::detect

    fn Specification::detect(contents : Json, default? : Specification) -> Specification raise ReferencingError

    Attempt to discern which specification applies to the given contents.

    Mirrors Python's Specification.detect, which may be called either as a class method (here: no default) or as an instance method (here: default=that_specification). Recall that not all contents contain enough information about which specification they are written for -- the JSON Schema {}, for instance, is valid under many different dialects.

    • Without default, raises CannotDetermineSpecification if the contents are not an object or have no string $schema, and UnknownDialect if $schema names an unknown dialect.
    • With default, that specification is returned for unidentifiable or unknown-dialect contents.

    Specification::id_of

    fn Specification::id_of(self : Specification, contents : Json) -> String?

    Find the ID of a given document.

    Specification::maybe_in_subresource

    fn Specification::maybe_in_subresource(self : Specification, segments : Array[Segment], resolver : Resolver, subresource : Resource) -> Resolver raise

    Conditionally enter a subresource while resolving a JSON pointer.

    Specification::new

    fn Specification::new(name~ : String, id_of~ : (Json) -> String?, subresources_of~ : (Json) -> Array[Json], anchors_in~ : (Specification, Json) -> Array[Anchor], maybe_in_subresource~ : (Array[Segment], Resolver, Resource) -> Resolver raise) -> Specification

    Create a new specification from its callbacks.

    Specification::subresources_of

    fn Specification::subresources_of(self : Specification, contents : Json) -> Array[Json]

    Retrieve the subresources of the given document.

    anchors_2019

    fn anchors_2019(specification : Specification, contents : Json) -> Array[Anchor]

    $anchor (draft 2019-09).

    Python's _anchor_2019.

    anchors_2020

    fn anchors_2020(specification : Specification, contents : Json) -> Array[Anchor]

    $anchor and $dynamicAnchor (draft 2020-12).

    Python's _anchor.

    dollar_id

    fn dollar_id(contents : Json) -> String?

    $id (draft 2019-09 and later): booleans have no ID.

    Python's _dollar_id.

    draft201909

    fn draft201909() -> Specification

    JSON Schema draft 2019-09.

    draft202012

    fn draft202012() -> Specification

    JSON Schema draft 2020-12.

    draft3

    fn draft3() -> Specification

    JSON Schema draft 3.

    draft4

    fn draft4() -> Specification

    JSON Schema draft 4.

    draft6

    fn draft6() -> Specification

    JSON Schema draft 6.

    draft7

    fn draft7() -> Specification

    JSON Schema draft 7.

    dynamic_anchor

    fn dynamic_anchor(name~ : String, resource~ : Resource) -> Anchor

    Create a dynamic anchor, introduced in draft 2020-12 (Python's DynamicAnchor(name=..., resource=...)). Its kind is "DynamicAnchor".

    Resolving it walks the resolver's dynamic scope (outermost last), taking the outermost resource which also declares a dynamic anchor with this name, and enters it as a subresource.

    empty_registry

    let empty_registry : Registry

    The empty JSON Schema registry (Python's EMPTY_REGISTRY).

    is_unresolvable

    fn is_unresolvable(error : Error) -> Bool

    Whether a generic Error is an Unresolvable (or a "subclass" thereof), i.e. whether Python's except referencing.exceptions.Unresolvable would catch it.

    legacy_anchor_in_dollar_id

    fn legacy_anchor_in_dollar_id(specification : Specification, contents : Json) -> Array[Anchor]

    Plain-name fragments in $id ({"$id": "#foo"}), drafts 6 and 7.

    Python's _legacy_anchor_in_dollar_id.

    legacy_anchor_in_id

    fn legacy_anchor_in_id(specification : Specification, contents : Json) -> Array[Anchor]

    Plain-name fragments in id ({"id": "#foo"}), drafts 3 and 4.

    Python's _legacy_anchor_in_id.

    legacy_dollar_id

    fn legacy_dollar_id(contents : Json) -> String?

    $id in drafts 6 and 7: ignored next to $ref (whose siblings are ignored in those drafts), and plain-name fragments (#foo) are anchors, not IDs.

    Python's _legacy_dollar_id.

    legacy_id

    fn legacy_id(contents : Json) -> String?

    id in drafts 3 and 4: ignored next to $ref, and plain-name fragments (#foo) are anchors, not IDs.

    Python's _legacy_id.

    lookup_recursive_ref

    fn lookup_recursive_ref(resolver : Resolver) -> Resolved raise

    Recursive references (via recursive anchors), present only in draft 2019-09.

    As per the 2019 specification (§ 8.2.4.2.1), only the # recursive reference is supported (and is therefore assumed to be the relevant reference).

    lru_cache

    fn lru_cache(maxsize~ : Int) -> (((String) -> Resource raise) -> ((String) -> Resource raise))

    A least-recently-used cache holding at most maxsize resources (Python's functools.lru_cache(maxsize=maxsize)). Errors are not cached.

    opaque_specification

    let opaque_specification : Specification

    An opaque specification where resources have no subresources nor internal identifiers (Python's Specification.OPAQUE).

    specification_with

    fn specification_with(dialect_id : String, default? : Specification) -> Specification raise ReferencingError

    Retrieve the Specification with the given dialect identifier (trailing #s are ignored).

    Raises UnknownDialect if the given dialect_id isn't known and no default is given.

    test {
    let spec = @referencing.specification_with(
    "http://json-schema.org/draft-07/schema#",
    )
    inspect(spec, content="<Specification name='draft-07'>")
    }

    to_cached_resource

    fn to_cached_resource(retrieve : (String) -> String raise, cache? : ((String) -> Resource raise) -> ((String) -> Resource raise), loads? : (String) -> Json raise, from_contents? : (Json) -> Resource raise) -> ((String) -> Resource raise)

    Create a retriever which caches its return values from a simpler function returning serialized documents (Python's referencing.retrieval.to_cached_resource(cache, loads, from_contents) applied to retrieve).

    • loads deserializes the document (default: @json.parse).
    • from_contents creates the resource (default: Resource::from_contents without a default specification).
    • cache is the caching strategy (default: unbounded_cache()).

    The result is suitable for Registry::new(retrieve=...).

    test {
    let calls = []
    let retrieve = @referencing.to_cached_resource(uri => {
    calls.push(uri)
    "{\"$schema\": \"https://json-schema.org/draft/2020-12/schema\", \"foo\": \"bar\"}"
    })
    let one = @referencing.Registry::new(retrieve~).get_or_retrieve(
    "urn:example:foo",
    )
    let two = @referencing.Registry::new(retrieve~).get_or_retrieve(
    "urn:example:foo",
    )
    inspect(
    one.value.contents.stringify(),
    content=(
    #|{"$schema":"https://json-schema.org/draft/2020-12/schema","foo":"bar"}
    ),
    )
    assert_true(physical_equal(one.value, two.value))
    assert_eq(calls, ["urn:example:foo"])
    }

    unbounded_cache

    fn unbounded_cache() -> (((String) -> Resource raise) -> ((String) -> Resource raise))

    An unbounded cache (Python's lru_cache(maxsize=None)). Errors are not cached.