Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,20 @@ minor versions may change public API.
shadowing: because a dynamic anchor is also a plain anchor, `$anchor: "n"`
on one object and `$dynamicAnchor: "n"` on another left `$ref` and
`$dynamicRef` resolving the same fragment to *different* schemas.
Re-registering an *equal* document is still a no-op, and one object carrying
both `$anchor` and `$dynamicAnchor` under one name is still fine. No case in
Re-registering an *equal* document is still allowed; it is walked again, and
its source-range lookup is replaced — dropped, when the re-registration
supplies none, since equal JSON can come from differently formatted text.
One object carrying both `$anchor` and `$dynamicAnchor` under one name is
still fine. No case in
the official test suite, the bundled metaschemas, or this repo's fixtures is
affected. **Breaking:** a document that registered before may now raise, and
a *modified* document can no longer be re-registered under its URI — there
is not yet a way to replace or unregister one, so an edit-and-re-register
loop needs a fresh engine (DESIGN.md §7, "No way to replace a registered
document"). **Known gaps:** three shapes still shadow silently — an embedded
`$id` equal to another document's retrieval URI, a retrieval URI reused for
a different document, and a bundled metaschema's URI, which can be claimed
only before that metaschema is first used (DESIGN.md §7, "P12 has three gaps
where an identifier still shadows silently").
a *modified* document can no longer be re-registered under its URI;
unregister it first with `Engine.unregister_schema`. Retrieval URIs and bundled metaschema URIs are claims too
(DESIGN.md P15): an `$id` equal to another document's retrieval URI, a
retrieval URI reused for a different document, and a bundled metaschema's
URI claimed with different content are all `DuplicateResourceError`. The
last used to succeed on a fresh engine and fail once the metaschema had been
used.
- **Registration is now all-or-nothing (DESIGN.md P13).** A document whose
registration raises leaves the registry exactly as it found it, so an engine
stays usable after a caught registration error. Previously the half-indexed
Expand Down Expand Up @@ -104,6 +106,13 @@ minor versions may change public API.

### Added

- **`Engine.unregister_schema(uri)` (DESIGN.md P15).** It removes a
registered document and everything its registration claimed: embedded `$id`
resources, anchors, recursive roots, dialect and location entries, its
source-range lookup, and its retrieval aliases. Unregister then register is
how a document is replaced. A resource that an equal copy in a later document
has taken over stays with that document. Compiled artifacts keep what they
were compiled against. The shape matches the TypeScript engine's.
- **Location chains (DESIGN.md P11).** A canonical `base_uri#pointer` names a
position exactly and still may not locate it: in a bundled document the base
may be an embedded `$id` the reader never knew was there, and a relative
Expand Down
91 changes: 21 additions & 70 deletions DESIGN.md

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions docs/guide/loaders.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,36 @@ assert bundle_engine.evaluate(bundle_uri, {"city": "NYC"}).valid is True
assert bundle_engine.evaluate(bundle_uri, {"city": 1}).valid is False
```

## Replacing a registered schema

A URI names one schema. Registering a *different* document under a URI that
is already registered raises `DuplicateResourceError`, whether it arrives
through `register_schema` or from a loader, so nothing is silently shadowed.
Registering an equal document again is fine. To replace one, for example an
editor re-validating on save, unregister it first. That removes the document
and everything it declared, including embedded `$id` resources, anchors, and
its retrieval-URI aliases:

```python
from json_schema_engine.core import DuplicateResourceError

editing = create_engine()
draft_uri = editing.register_schema({"type": "string"}, "https://example.com/draft")
try:
editing.register_schema({"type": "integer"}, draft_uri)
replaced_silently = True
except DuplicateResourceError:
replaced_silently = False
assert replaced_silently is False

editing.unregister_schema(draft_uri)
editing.register_schema({"type": "integer"}, draft_uri)
assert editing.evaluate(draft_uri, 1).valid is True
```

A compiled artifact keeps the schemas it was compiled against, so compile
again after a replacement.

## Bundled metaschemas never go through loaders

The standard metaschemas (2020-12, 2019-09, draft-07, draft-06, and the
Expand Down
20 changes: 20 additions & 0 deletions docs/guide/metaschemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,26 @@ assert no_loader_engine.evaluate(meta_uri, {"type": "integer"}).valid is True
assert no_loader_engine.evaluate(meta_uri, {"type": 1}).valid is False
```

A bundled URI is reserved for its bundled content. You can register an
equal copy, but a different document under one of these URIs is a
`DuplicateResourceError`, whether or not the metaschema has been used yet. A
bundled metaschema cannot be unregistered either. A customized metaschema
goes under a URI of its own, as the `$vocabulary` section above does.

```python
from json_schema_engine.core import DuplicateResourceError

try:
create_engine().register_schema(
{"$id": "https://json-schema.org/draft/2020-12/schema", "type": "object"},
"https://ex.example/my-meta",
)
overridden = True
except DuplicateResourceError:
overridden = False
assert overridden is False
```

See [Loaders](loaders.md) for how a `$ref` to a *non*-bundled resource
gets resolved instead.

Expand Down
25 changes: 22 additions & 3 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ same keyword-only parameters. Registration is synchronous and local;
registered or bundled metaschema; `$ref` targets are not followed (use
`load_schema` for that). `get_range` is the D17 position capability for this
document.
- `Engine.unregister_schema(uri) -> None`: removes a registered document and
everything its registration claimed: embedded `$id` resources, anchors and
dynamic anchors, recursive roots, dialect and location entries, its range
lookup, and every retrieval alias for it (P15). `uri` is the document's
canonical or retrieval URI. A resource that an equal copy in a later document
has since taken over stays with that document. Unregister then
`register_schema` is how a document is replaced. Compiled artifacts keep what
they were compiled against, and a dialect assembled from an unregistered
metaschema stays registered. Raises `UnresolvableReferenceError` for a URI
that names no document root (including a resource embedded in another
document, which the message names), and `ReadOnlyRegistryError` for a
bundled metaschema.
- `Engine.load_schema(schema, retrieval_uri, dialect_uri=None, get_range=None)
-> str`: registers a document and loads every resource it references (P4).
- `Engine.load(uri) -> str`: loads and registers a resource by URI through the
Expand Down Expand Up @@ -227,7 +239,12 @@ about two positions claiming one URI.
`DuplicateResourceError`: two different schemas claim one resource URI (P12) —
either a single document minting the same `$id` twice, or a later registration
that would rebind a URI an earlier one bound to a different schema.
Re-registering an *equal* document is a no-op, not a duplicate.
Re-registering an *equal* document is not a duplicate. A retrieval URI is a
claim too (P15): an `$id` equal to another document's retrieval URI, and a
retrieval URI that already names a resource or aliases a different one, are
refused, since one of the two would be unreachable. A bundled metaschema's URI
is reserved for content equal to the bundled document, whether or not that
metaschema has been used yet; a custom metaschema goes under a URI of its own.

`DuplicateAnchorError`: two different schema objects claim one anchor name
within a resource (P12). Covers `$anchor`, `$dynamicAnchor`, and the
Expand All @@ -242,8 +259,10 @@ caught registration error. Because the document is gone, `Engine.locate` cannot
place such an error afterwards — the error carries its own `schema_source`,
captured before the rollback.

`ReadOnlyRegistryError`: registration was attempted on a compiled artifact's
registry snapshot.
`ReadOnlyRegistryError`: a registry, or an entry in one, cannot be modified:
registration or unregistration on a compiled artifact's registry snapshot,
unregistering from caller code running inside a registration, or unregistering
a bundled metaschema (P15).

`UnknownDialectError`: a schema names a `$schema` dialect URI that no
registered dialect claims, or that cannot be assembled. `dialect_uri: str |
Expand Down
10 changes: 10 additions & 0 deletions src/json_schema_engine/core/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,16 @@ def _register_assembling_dialects(
failure.schema_source = error.schema_source
raise

def unregister_schema(self, uri: str) -> None:
"""Remove a registered document and everything it claimed (P15).

`uri` is the document's canonical URI or its retrieval URI. Unregister
then `register_schema` is how a registered document is replaced. A
compiled artifact keeps the schemas it was compiled against, and a
dialect assembled from an unregistered metaschema stays registered.
"""
self.schemas.unregister(uri)

def load_schema(
self,
schema: JsonValue,
Expand Down
7 changes: 6 additions & 1 deletion src/json_schema_engine/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,12 @@ class DuplicateAnchorError(JsonSchemaEngineError):


class ReadOnlyRegistryError(JsonSchemaEngineError):
"""Registration attempted on a compiled artifact's registry snapshot."""
"""A registry, or an entry in one, cannot be modified.

Raised for registration or unregistration on a compiled artifact's
registry snapshot, for unregistering while a registration is in
progress, and for unregistering a bundled metaschema (P15).
"""


class UnknownDialectError(JsonSchemaEngineError):
Expand Down
Loading
Loading