Skip to content

Commit f062746

Browse files
committed
Initial swipe at specification detection.
1 parent 9df9c05 commit f062746

File tree

5 files changed

+158
-12
lines changed

5 files changed

+158
-12
lines changed

referencing/__init__.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
"""
22
Cross-specification, implementation-agnostic JSON referencing.
33
"""
4-
from referencing._core import Registry, Resource, Specification
4+
from referencing._core import (
5+
CannotDetermineSpecification,
6+
Registry,
7+
Resource,
8+
Specification,
9+
)
510

6-
__all__ = ["Registry", "Resource", "Specification"]
11+
__all__ = [
12+
"CannotDetermineSpecification",
13+
"Registry",
14+
"Resource",
15+
"Specification",
16+
]

referencing/_core.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

3-
from collections.abc import Iterable
4-
from typing import Callable, ClassVar, Generic
3+
from collections.abc import Iterable, Mapping
4+
from typing import Any, Callable, ClassVar, Generic
55

66
from attrs import evolve
77
from pyrsistent import pmap
@@ -11,6 +11,18 @@
1111
from referencing.typing import URI, D
1212

1313

14+
@frozen
15+
class CannotDetermineSpecification(Exception):
16+
"""
17+
Attempting to detect the appropriate `Specification` failed.
18+
19+
This happens if no discernible information is found in the contents of the
20+
new resource which would help identify it.
21+
"""
22+
23+
contents: Any
24+
25+
1426
@frozen
1527
class Specification:
1628
"""
@@ -45,11 +57,25 @@ class Resource(Generic[D]):
4557
_specification: Specification
4658

4759
@classmethod
48-
def from_contents(cls, contents: D) -> Resource[D]:
60+
def from_contents(
61+
cls,
62+
contents: D,
63+
default_specification: Specification = ..., # type: ignore[assignment]
64+
) -> Resource[D]:
4965
"""
5066
Attempt to discern which specification applies to the given contents.
5167
"""
52-
return cls(contents=contents, specification=Specification.OPAQUE)
68+
specification = default_specification
69+
if isinstance(contents, Mapping):
70+
jsonschema_dialect_identifier = contents.get("$schema")
71+
if jsonschema_dialect_identifier is not None:
72+
from referencing import jsonschema
73+
74+
specification = jsonschema.BY_ID[jsonschema_dialect_identifier]
75+
76+
if specification is ...: # type: ignore[comparison-overlap]
77+
raise CannotDetermineSpecification(contents)
78+
return cls(contents=contents, specification=specification)
5379

5480
def id(self) -> str | None:
5581
"""

referencing/jsonschema.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,22 @@
33
"""
44

55
from __future__ import annotations
6+
7+
from referencing import Specification
8+
9+
DRAFT202012 = Specification(id_of=lambda contents: None)
10+
DRAFT201909 = Specification(id_of=lambda contents: None)
11+
DRAFT7 = Specification(id_of=lambda contents: None)
12+
DRAFT6 = Specification(id_of=lambda contents: None)
13+
DRAFT4 = Specification(id_of=lambda contents: None)
14+
DRAFT3 = Specification(id_of=lambda contents: None)
15+
16+
17+
BY_ID = {
18+
"https://json-schema.org/draft/2020-12/schema": DRAFT202012,
19+
"https://json-schema.org/draft/2019-09/schema": DRAFT201909,
20+
"http://json-schema.org/draft-07/schema#": DRAFT7,
21+
"http://json-schema.org/draft-06/schema#": DRAFT6,
22+
"http://json-schema.org/draft-04/schema#": DRAFT4,
23+
"http://json-schema.org/draft-03/schema#": DRAFT3,
24+
}

referencing/tests/test_core.py

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
from referencing import Registry, Resource, Specification
1+
import pytest
2+
3+
from referencing import (
4+
CannotDetermineSpecification,
5+
Registry,
6+
Resource,
7+
Specification,
8+
)
29

310

411
def test_registry_with_resource():
@@ -25,14 +32,56 @@ def test_registry_crawl_still_has_top_level_resource():
2532
assert registry[uri] is resource
2633

2734

28-
def test_resource_from_contents_with_no_discernable_information():
35+
def test_resource_from_contents_with_no_discernible_information():
2936
"""
30-
Creating a resource with no discernable way to see what specification it
31-
belongs to (e.g. no $schema keyword for JSON Schema) creates one with an
32-
opaque specification.
37+
Creating a resource with no discernible way to see what specification it
38+
belongs to (e.g. no $schema keyword for JSON Schema) raises an error.
3339
"""
3440

35-
resource = Resource.from_contents({"foo": "bar"})
41+
with pytest.raises(CannotDetermineSpecification):
42+
Resource.from_contents({"foo": "bar"})
43+
44+
45+
def test_resource_from_contents_with_no_discernible_information_and_default():
46+
resource = Resource.from_contents(
47+
{"foo": "bar"},
48+
default_specification=Specification.OPAQUE,
49+
)
50+
assert resource == Resource(
51+
contents={"foo": "bar"},
52+
specification=Specification.OPAQUE,
53+
)
54+
55+
56+
def test_resource_from_contents_unneeded_default():
57+
from referencing.jsonschema import DRAFT202012
58+
59+
resource = Resource.from_contents(
60+
{"$schema": "https://json-schema.org/draft/2020-12/schema"},
61+
default_specification=Specification.OPAQUE,
62+
)
63+
assert resource == Resource(
64+
contents={"$schema": "https://json-schema.org/draft/2020-12/schema"},
65+
specification=DRAFT202012,
66+
)
67+
68+
69+
def test_non_mapping_resource_from_contents_with_no_discernible_information():
70+
resource = Resource.from_contents(
71+
True,
72+
default_specification=Specification.OPAQUE,
73+
)
74+
assert resource == Resource(
75+
contents=True,
76+
specification=Specification.OPAQUE,
77+
)
78+
79+
80+
def test_resource_from_contents_with_fallback():
81+
resource = Resource.from_contents(
82+
{"foo": "bar"},
83+
default_specification=Specification.OPAQUE,
84+
)
3685
assert resource == Resource(
3786
contents={"foo": "bar"},
3887
specification=Specification.OPAQUE,

referencing/tests/test_jsonschema.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import pytest
2+
3+
from referencing import Resource
4+
import referencing.jsonschema
5+
6+
7+
@pytest.mark.parametrize(
8+
"uri, expected",
9+
[
10+
(
11+
"https://json-schema.org/draft/2020-12/schema",
12+
referencing.jsonschema.DRAFT202012,
13+
),
14+
(
15+
"https://json-schema.org/draft/2019-09/schema",
16+
referencing.jsonschema.DRAFT201909,
17+
),
18+
(
19+
"http://json-schema.org/draft-07/schema#",
20+
referencing.jsonschema.DRAFT7,
21+
),
22+
(
23+
"http://json-schema.org/draft-06/schema#",
24+
referencing.jsonschema.DRAFT6,
25+
),
26+
(
27+
"http://json-schema.org/draft-04/schema#",
28+
referencing.jsonschema.DRAFT4,
29+
),
30+
(
31+
"http://json-schema.org/draft-03/schema#",
32+
referencing.jsonschema.DRAFT3,
33+
),
34+
],
35+
)
36+
def test_schemas_with_explicit_schema_keywords_are_detected(uri, expected):
37+
"""
38+
The $schema keyword in JSON Schema is a dialect identifier.
39+
"""
40+
contents = {"$schema": uri}
41+
resource = Resource.from_contents(contents)
42+
assert resource == Resource(contents=contents, specification=expected)

0 commit comments

Comments
 (0)