test_core.py 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057
  1. from rpds import HashTrieMap
  2. import pytest
  3. from referencing import Anchor, Registry, Resource, Specification, exceptions
  4. from referencing.jsonschema import DRAFT202012
  5. ID_AND_CHILDREN = Specification(
  6. name="id-and-children",
  7. id_of=lambda contents: contents.get("ID"),
  8. subresources_of=lambda contents: contents.get("children", []),
  9. anchors_in=lambda specification, contents: [
  10. Anchor(
  11. name=name,
  12. resource=specification.create_resource(contents=each),
  13. )
  14. for name, each in contents.get("anchors", {}).items()
  15. ],
  16. maybe_in_subresource=lambda segments, resolver, subresource: (
  17. resolver.in_subresource(subresource)
  18. if not len(segments) % 2
  19. and all(each == "children" for each in segments[::2])
  20. else resolver
  21. ),
  22. )
  23. def blow_up(uri): # pragma: no cover
  24. """
  25. A retriever suitable for use in tests which expect it never to be used.
  26. """
  27. raise RuntimeError("This retrieve function expects to never be called!")
  28. class TestRegistry:
  29. def test_with_resource(self):
  30. """
  31. Adding a resource to the registry then allows re-retrieving it.
  32. """
  33. resource = Resource.opaque(contents={"foo": "bar"})
  34. uri = "urn:example"
  35. registry = Registry().with_resource(uri=uri, resource=resource)
  36. assert registry[uri] is resource
  37. def test_with_resources(self):
  38. """
  39. Adding multiple resources to the registry is like adding each one.
  40. """
  41. one = Resource.opaque(contents={})
  42. two = Resource(contents={"foo": "bar"}, specification=ID_AND_CHILDREN)
  43. registry = Registry().with_resources(
  44. [
  45. ("http://example.com/1", one),
  46. ("http://example.com/foo/bar", two),
  47. ],
  48. )
  49. assert registry == Registry().with_resource(
  50. uri="http://example.com/1",
  51. resource=one,
  52. ).with_resource(
  53. uri="http://example.com/foo/bar",
  54. resource=two,
  55. )
  56. def test_matmul_resource(self):
  57. uri = "urn:example:resource"
  58. resource = ID_AND_CHILDREN.create_resource({"ID": uri, "foo": 12})
  59. registry = resource @ Registry()
  60. assert registry == Registry().with_resource(uri, resource)
  61. def test_matmul_many_resources(self):
  62. one_uri = "urn:example:one"
  63. one = ID_AND_CHILDREN.create_resource({"ID": one_uri, "foo": 12})
  64. two_uri = "urn:example:two"
  65. two = ID_AND_CHILDREN.create_resource({"ID": two_uri, "foo": 12})
  66. registry = [one, two] @ Registry()
  67. assert registry == Registry().with_resources(
  68. [(one_uri, one), (two_uri, two)],
  69. )
  70. def test_matmul_resource_without_id(self):
  71. resource = Resource.opaque(contents={"foo": "bar"})
  72. with pytest.raises(exceptions.NoInternalID) as e:
  73. resource @ Registry()
  74. assert e.value == exceptions.NoInternalID(resource=resource)
  75. def test_with_contents_from_json_schema(self):
  76. uri = "urn:example"
  77. schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
  78. registry = Registry().with_contents([(uri, schema)])
  79. expected = Resource(contents=schema, specification=DRAFT202012)
  80. assert registry[uri] == expected
  81. def test_with_contents_and_default_specification(self):
  82. uri = "urn:example"
  83. registry = Registry().with_contents(
  84. [(uri, {"foo": "bar"})],
  85. default_specification=Specification.OPAQUE,
  86. )
  87. assert registry[uri] == Resource.opaque({"foo": "bar"})
  88. def test_len(self):
  89. total = 5
  90. registry = Registry().with_contents(
  91. [(str(i), {"foo": "bar"}) for i in range(total)],
  92. default_specification=Specification.OPAQUE,
  93. )
  94. assert len(registry) == total
  95. def test_bool_empty(self):
  96. assert not Registry()
  97. def test_bool_not_empty(self):
  98. registry = Registry().with_contents(
  99. [(str(i), {"foo": "bar"}) for i in range(3)],
  100. default_specification=Specification.OPAQUE,
  101. )
  102. assert registry
  103. def test_iter(self):
  104. registry = Registry().with_contents(
  105. [(str(i), {"foo": "bar"}) for i in range(8)],
  106. default_specification=Specification.OPAQUE,
  107. )
  108. assert set(registry) == {str(i) for i in range(8)}
  109. def test_crawl_still_has_top_level_resource(self):
  110. resource = Resource.opaque({"foo": "bar"})
  111. uri = "urn:example"
  112. registry = Registry({uri: resource}).crawl()
  113. assert registry[uri] is resource
  114. def test_crawl_finds_a_subresource(self):
  115. child_id = "urn:child"
  116. root = ID_AND_CHILDREN.create_resource(
  117. {"ID": "urn:root", "children": [{"ID": child_id, "foo": 12}]},
  118. )
  119. registry = root @ Registry()
  120. with pytest.raises(LookupError):
  121. registry[child_id]
  122. expected = ID_AND_CHILDREN.create_resource({"ID": child_id, "foo": 12})
  123. assert registry.crawl()[child_id] == expected
  124. def test_crawl_finds_anchors_with_id(self):
  125. resource = ID_AND_CHILDREN.create_resource(
  126. {"ID": "urn:bar", "anchors": {"foo": 12}},
  127. )
  128. registry = resource @ Registry()
  129. assert registry.crawl().anchor(resource.id(), "foo").value == Anchor(
  130. name="foo",
  131. resource=ID_AND_CHILDREN.create_resource(12),
  132. )
  133. def test_crawl_finds_anchors_no_id(self):
  134. resource = ID_AND_CHILDREN.create_resource({"anchors": {"foo": 12}})
  135. registry = Registry().with_resource("urn:root", resource)
  136. assert registry.crawl().anchor("urn:root", "foo").value == Anchor(
  137. name="foo",
  138. resource=ID_AND_CHILDREN.create_resource(12),
  139. )
  140. def test_contents(self):
  141. resource = Resource.opaque({"foo": "bar"})
  142. uri = "urn:example"
  143. registry = Registry().with_resource(uri, resource)
  144. assert registry.contents(uri) == {"foo": "bar"}
  145. def test_getitem_strips_empty_fragments(self):
  146. uri = "http://example.com/"
  147. resource = ID_AND_CHILDREN.create_resource({"ID": uri + "#"})
  148. registry = resource @ Registry()
  149. assert registry[uri] == registry[uri + "#"] == resource
  150. def test_contents_strips_empty_fragments(self):
  151. uri = "http://example.com/"
  152. resource = ID_AND_CHILDREN.create_resource({"ID": uri + "#"})
  153. registry = resource @ Registry()
  154. assert (
  155. registry.contents(uri)
  156. == registry.contents(uri + "#")
  157. == {"ID": uri + "#"}
  158. )
  159. def test_contents_nonexistent_resource(self):
  160. registry = Registry()
  161. with pytest.raises(exceptions.NoSuchResource) as e:
  162. registry.contents("urn:example")
  163. assert e.value == exceptions.NoSuchResource(ref="urn:example")
  164. def test_crawled_anchor(self):
  165. resource = ID_AND_CHILDREN.create_resource({"anchors": {"foo": "bar"}})
  166. registry = Registry().with_resource("urn:example", resource)
  167. retrieved = registry.anchor("urn:example", "foo")
  168. assert retrieved.value == Anchor(
  169. name="foo",
  170. resource=ID_AND_CHILDREN.create_resource("bar"),
  171. )
  172. assert retrieved.registry == registry.crawl()
  173. def test_anchor_in_nonexistent_resource(self):
  174. registry = Registry()
  175. with pytest.raises(exceptions.NoSuchResource) as e:
  176. registry.anchor("urn:example", "foo")
  177. assert e.value == exceptions.NoSuchResource(ref="urn:example")
  178. def test_init(self):
  179. one = Resource.opaque(contents={})
  180. two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
  181. registry = Registry(
  182. {
  183. "http://example.com/1": one,
  184. "http://example.com/foo/bar": two,
  185. },
  186. )
  187. assert (
  188. registry
  189. == Registry()
  190. .with_resources(
  191. [
  192. ("http://example.com/1", one),
  193. ("http://example.com/foo/bar", two),
  194. ],
  195. )
  196. .crawl()
  197. )
  198. def test_dict_conversion(self):
  199. """
  200. Passing a `dict` to `Registry` gets converted to a `HashTrieMap`.
  201. So continuing to use the registry works.
  202. """
  203. one = Resource.opaque(contents={})
  204. two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
  205. registry = Registry(
  206. {"http://example.com/1": one},
  207. ).with_resource("http://example.com/foo/bar", two)
  208. assert (
  209. registry.crawl()
  210. == Registry()
  211. .with_resources(
  212. [
  213. ("http://example.com/1", one),
  214. ("http://example.com/foo/bar", two),
  215. ],
  216. )
  217. .crawl()
  218. )
  219. def test_no_such_resource(self):
  220. registry = Registry()
  221. with pytest.raises(exceptions.NoSuchResource) as e:
  222. registry["urn:bigboom"]
  223. assert e.value == exceptions.NoSuchResource(ref="urn:bigboom")
  224. def test_combine(self):
  225. one = Resource.opaque(contents={})
  226. two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
  227. three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
  228. four = ID_AND_CHILDREN.create_resource({"anchors": {"foo": 12}})
  229. first = Registry({"http://example.com/1": one})
  230. second = Registry().with_resource("http://example.com/foo/bar", two)
  231. third = Registry(
  232. {
  233. "http://example.com/1": one,
  234. "http://example.com/baz": three,
  235. },
  236. )
  237. fourth = (
  238. Registry()
  239. .with_resource(
  240. "http://example.com/foo/quux",
  241. four,
  242. )
  243. .crawl()
  244. )
  245. assert first.combine(second, third, fourth) == Registry(
  246. [
  247. ("http://example.com/1", one),
  248. ("http://example.com/baz", three),
  249. ("http://example.com/foo/quux", four),
  250. ],
  251. anchors=HashTrieMap(
  252. {
  253. ("http://example.com/foo/quux", "foo"): Anchor(
  254. name="foo",
  255. resource=ID_AND_CHILDREN.create_resource(12),
  256. ),
  257. },
  258. ),
  259. ).with_resource("http://example.com/foo/bar", two)
  260. def test_combine_self(self):
  261. """
  262. Combining a registry with itself short-circuits.
  263. This is a performance optimization -- otherwise we do lots more work
  264. (in jsonschema this seems to correspond to making the test suite take
  265. *3x* longer).
  266. """
  267. registry = Registry({"urn:foo": "bar"})
  268. assert registry.combine(registry) is registry
  269. def test_combine_with_uncrawled_resources(self):
  270. one = Resource.opaque(contents={})
  271. two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
  272. three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
  273. first = Registry().with_resource("http://example.com/1", one)
  274. second = Registry().with_resource("http://example.com/foo/bar", two)
  275. third = Registry(
  276. {
  277. "http://example.com/1": one,
  278. "http://example.com/baz": three,
  279. },
  280. )
  281. expected = Registry(
  282. [
  283. ("http://example.com/1", one),
  284. ("http://example.com/foo/bar", two),
  285. ("http://example.com/baz", three),
  286. ],
  287. )
  288. combined = first.combine(second, third)
  289. assert combined != expected
  290. assert combined.crawl() == expected
  291. def test_combine_with_single_retrieve(self):
  292. one = Resource.opaque(contents={})
  293. two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
  294. three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
  295. def retrieve(uri): # pragma: no cover
  296. pass
  297. first = Registry().with_resource("http://example.com/1", one)
  298. second = Registry(
  299. retrieve=retrieve,
  300. ).with_resource("http://example.com/2", two)
  301. third = Registry().with_resource("http://example.com/3", three)
  302. assert first.combine(second, third) == Registry(
  303. retrieve=retrieve,
  304. ).with_resources(
  305. [
  306. ("http://example.com/1", one),
  307. ("http://example.com/2", two),
  308. ("http://example.com/3", three),
  309. ],
  310. )
  311. assert second.combine(first, third) == Registry(
  312. retrieve=retrieve,
  313. ).with_resources(
  314. [
  315. ("http://example.com/1", one),
  316. ("http://example.com/2", two),
  317. ("http://example.com/3", three),
  318. ],
  319. )
  320. def test_combine_with_common_retrieve(self):
  321. one = Resource.opaque(contents={})
  322. two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
  323. three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
  324. def retrieve(uri): # pragma: no cover
  325. pass
  326. first = Registry(retrieve=retrieve).with_resource(
  327. "http://example.com/1",
  328. one,
  329. )
  330. second = Registry(
  331. retrieve=retrieve,
  332. ).with_resource("http://example.com/2", two)
  333. third = Registry(retrieve=retrieve).with_resource(
  334. "http://example.com/3",
  335. three,
  336. )
  337. assert first.combine(second, third) == Registry(
  338. retrieve=retrieve,
  339. ).with_resources(
  340. [
  341. ("http://example.com/1", one),
  342. ("http://example.com/2", two),
  343. ("http://example.com/3", three),
  344. ],
  345. )
  346. assert second.combine(first, third) == Registry(
  347. retrieve=retrieve,
  348. ).with_resources(
  349. [
  350. ("http://example.com/1", one),
  351. ("http://example.com/2", two),
  352. ("http://example.com/3", three),
  353. ],
  354. )
  355. def test_combine_conflicting_retrieve(self):
  356. one = Resource.opaque(contents={})
  357. two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
  358. three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
  359. def foo_retrieve(uri): # pragma: no cover
  360. pass
  361. def bar_retrieve(uri): # pragma: no cover
  362. pass
  363. first = Registry(retrieve=foo_retrieve).with_resource(
  364. "http://example.com/1",
  365. one,
  366. )
  367. second = Registry().with_resource("http://example.com/2", two)
  368. third = Registry(retrieve=bar_retrieve).with_resource(
  369. "http://example.com/3",
  370. three,
  371. )
  372. with pytest.raises(Exception, match="conflict.*retriev"):
  373. first.combine(second, third)
  374. def test_remove(self):
  375. one = Resource.opaque(contents={})
  376. two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
  377. registry = Registry({"urn:foo": one, "urn:bar": two})
  378. assert registry.remove("urn:foo") == Registry({"urn:bar": two})
  379. def test_remove_uncrawled(self):
  380. one = Resource.opaque(contents={})
  381. two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
  382. registry = Registry().with_resources(
  383. [("urn:foo", one), ("urn:bar", two)],
  384. )
  385. assert registry.remove("urn:foo") == Registry().with_resource(
  386. "urn:bar",
  387. two,
  388. )
  389. def test_remove_with_anchors(self):
  390. one = Resource.opaque(contents={})
  391. two = ID_AND_CHILDREN.create_resource({"anchors": {"foo": "bar"}})
  392. registry = (
  393. Registry()
  394. .with_resources(
  395. [("urn:foo", one), ("urn:bar", two)],
  396. )
  397. .crawl()
  398. )
  399. assert (
  400. registry.remove("urn:bar")
  401. == Registry()
  402. .with_resource(
  403. "urn:foo",
  404. one,
  405. )
  406. .crawl()
  407. )
  408. def test_remove_nonexistent_uri(self):
  409. with pytest.raises(exceptions.NoSuchResource) as e:
  410. Registry().remove("urn:doesNotExist")
  411. assert e.value == exceptions.NoSuchResource(ref="urn:doesNotExist")
  412. def test_retrieve(self):
  413. foo = Resource.opaque({"foo": "bar"})
  414. registry = Registry(retrieve=lambda uri: foo)
  415. assert registry.get_or_retrieve("urn:example").value == foo
  416. def test_retrieve_arbitrary_exception(self):
  417. foo = Resource.opaque({"foo": "bar"})
  418. def retrieve(uri):
  419. if uri == "urn:succeed":
  420. return foo
  421. raise Exception("Oh no!")
  422. registry = Registry(retrieve=retrieve)
  423. assert registry.get_or_retrieve("urn:succeed").value == foo
  424. with pytest.raises(exceptions.Unretrievable):
  425. registry.get_or_retrieve("urn:uhoh")
  426. def test_retrieve_no_such_resource(self):
  427. foo = Resource.opaque({"foo": "bar"})
  428. def retrieve(uri):
  429. if uri == "urn:succeed":
  430. return foo
  431. raise exceptions.NoSuchResource(ref=uri)
  432. registry = Registry(retrieve=retrieve)
  433. assert registry.get_or_retrieve("urn:succeed").value == foo
  434. with pytest.raises(exceptions.NoSuchResource):
  435. registry.get_or_retrieve("urn:uhoh")
  436. def test_retrieve_cannot_determine_specification(self):
  437. def retrieve(uri):
  438. return Resource.from_contents({})
  439. registry = Registry(retrieve=retrieve)
  440. with pytest.raises(exceptions.CannotDetermineSpecification):
  441. registry.get_or_retrieve("urn:uhoh")
  442. def test_retrieve_already_available_resource(self):
  443. foo = Resource.opaque({"foo": "bar"})
  444. registry = Registry({"urn:example": foo}, retrieve=blow_up)
  445. assert registry["urn:example"] == foo
  446. assert registry.get_or_retrieve("urn:example").value == foo
  447. def test_retrieve_first_checks_crawlable_resource(self):
  448. child = ID_AND_CHILDREN.create_resource({"ID": "urn:child", "foo": 12})
  449. root = ID_AND_CHILDREN.create_resource({"children": [child.contents]})
  450. registry = Registry(retrieve=blow_up).with_resource("urn:root", root)
  451. assert registry.crawl()["urn:child"] == child
  452. def test_resolver(self):
  453. one = Resource.opaque(contents={})
  454. registry = Registry({"http://example.com": one})
  455. resolver = registry.resolver(base_uri="http://example.com")
  456. assert resolver.lookup("#").contents == {}
  457. def test_resolver_with_root_identified(self):
  458. root = ID_AND_CHILDREN.create_resource({"ID": "http://example.com"})
  459. resolver = Registry().resolver_with_root(root)
  460. assert resolver.lookup("http://example.com").contents == root.contents
  461. assert resolver.lookup("#").contents == root.contents
  462. def test_resolver_with_root_unidentified(self):
  463. root = Resource.opaque(contents={})
  464. resolver = Registry().resolver_with_root(root)
  465. assert resolver.lookup("#").contents == root.contents
  466. def test_repr(self):
  467. one = Resource.opaque(contents={})
  468. two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
  469. registry = Registry().with_resources(
  470. [
  471. ("http://example.com/1", one),
  472. ("http://example.com/foo/bar", two),
  473. ],
  474. )
  475. assert repr(registry) == "<Registry (2 uncrawled resources)>"
  476. assert repr(registry.crawl()) == "<Registry (2 resources)>"
  477. def test_repr_mixed_crawled(self):
  478. one = Resource.opaque(contents={})
  479. two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
  480. registry = (
  481. Registry(
  482. {"http://example.com/1": one},
  483. )
  484. .crawl()
  485. .with_resource(uri="http://example.com/foo/bar", resource=two)
  486. )
  487. assert repr(registry) == "<Registry (2 resources, 1 uncrawled)>"
  488. def test_repr_one_resource(self):
  489. registry = Registry().with_resource(
  490. uri="http://example.com/1",
  491. resource=Resource.opaque(contents={}),
  492. )
  493. assert repr(registry) == "<Registry (1 uncrawled resource)>"
  494. def test_repr_empty(self):
  495. assert repr(Registry()) == "<Registry (0 resources)>"
  496. class TestResource:
  497. def test_from_contents_from_json_schema(self):
  498. schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
  499. resource = Resource.from_contents(schema)
  500. assert resource == Resource(contents=schema, specification=DRAFT202012)
  501. def test_from_contents_with_no_discernible_information(self):
  502. """
  503. Creating a resource with no discernible way to see what
  504. specification it belongs to (e.g. no ``$schema`` keyword for JSON
  505. Schema) raises an error.
  506. """
  507. with pytest.raises(exceptions.CannotDetermineSpecification):
  508. Resource.from_contents({"foo": "bar"})
  509. def test_from_contents_with_no_discernible_information_and_default(self):
  510. resource = Resource.from_contents(
  511. {"foo": "bar"},
  512. default_specification=Specification.OPAQUE,
  513. )
  514. assert resource == Resource.opaque(contents={"foo": "bar"})
  515. def test_from_contents_unneeded_default(self):
  516. schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
  517. resource = Resource.from_contents(
  518. schema,
  519. default_specification=Specification.OPAQUE,
  520. )
  521. assert resource == Resource(
  522. contents=schema,
  523. specification=DRAFT202012,
  524. )
  525. def test_non_mapping_from_contents(self):
  526. resource = Resource.from_contents(
  527. True,
  528. default_specification=ID_AND_CHILDREN,
  529. )
  530. assert resource == Resource(
  531. contents=True,
  532. specification=ID_AND_CHILDREN,
  533. )
  534. def test_from_contents_with_fallback(self):
  535. resource = Resource.from_contents(
  536. {"foo": "bar"},
  537. default_specification=Specification.OPAQUE,
  538. )
  539. assert resource == Resource.opaque(contents={"foo": "bar"})
  540. def test_id_delegates_to_specification(self):
  541. specification = Specification(
  542. name="",
  543. id_of=lambda contents: "urn:fixedID",
  544. subresources_of=lambda contents: [],
  545. anchors_in=lambda specification, contents: [],
  546. maybe_in_subresource=(
  547. lambda segments, resolver, subresource: resolver
  548. ),
  549. )
  550. resource = Resource(
  551. contents={"foo": "baz"},
  552. specification=specification,
  553. )
  554. assert resource.id() == "urn:fixedID"
  555. def test_id_strips_empty_fragment(self):
  556. uri = "http://example.com/"
  557. root = ID_AND_CHILDREN.create_resource({"ID": uri + "#"})
  558. assert root.id() == uri
  559. def test_subresources_delegates_to_specification(self):
  560. resource = ID_AND_CHILDREN.create_resource({"children": [{}, 12]})
  561. assert list(resource.subresources()) == [
  562. ID_AND_CHILDREN.create_resource(each) for each in [{}, 12]
  563. ]
  564. def test_subresource_with_different_specification(self):
  565. schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
  566. resource = ID_AND_CHILDREN.create_resource({"children": [schema]})
  567. assert list(resource.subresources()) == [
  568. DRAFT202012.create_resource(schema),
  569. ]
  570. def test_anchors_delegates_to_specification(self):
  571. resource = ID_AND_CHILDREN.create_resource(
  572. {"anchors": {"foo": {}, "bar": 1, "baz": ""}},
  573. )
  574. assert list(resource.anchors()) == [
  575. Anchor(name="foo", resource=ID_AND_CHILDREN.create_resource({})),
  576. Anchor(name="bar", resource=ID_AND_CHILDREN.create_resource(1)),
  577. Anchor(name="baz", resource=ID_AND_CHILDREN.create_resource("")),
  578. ]
  579. def test_pointer_to_mapping(self):
  580. resource = Resource.opaque(contents={"foo": "baz"})
  581. resolver = Registry().resolver()
  582. assert resource.pointer("/foo", resolver=resolver).contents == "baz"
  583. def test_pointer_to_array(self):
  584. resource = Resource.opaque(contents={"foo": {"bar": [3]}})
  585. resolver = Registry().resolver()
  586. assert resource.pointer("/foo/bar/0", resolver=resolver).contents == 3
  587. def test_root_pointer(self):
  588. contents = {"foo": "baz"}
  589. resource = Resource.opaque(contents=contents)
  590. resolver = Registry().resolver()
  591. assert resource.pointer("", resolver=resolver).contents == contents
  592. def test_opaque(self):
  593. contents = {"foo": "bar"}
  594. assert Resource.opaque(contents) == Resource(
  595. contents=contents,
  596. specification=Specification.OPAQUE,
  597. )
  598. class TestResolver:
  599. def test_lookup_exact_uri(self):
  600. resource = Resource.opaque(contents={"foo": "baz"})
  601. resolver = Registry({"http://example.com/1": resource}).resolver()
  602. resolved = resolver.lookup("http://example.com/1")
  603. assert resolved.contents == resource.contents
  604. def test_lookup_subresource(self):
  605. root = ID_AND_CHILDREN.create_resource(
  606. {
  607. "ID": "http://example.com/",
  608. "children": [
  609. {"ID": "http://example.com/a", "foo": 12},
  610. ],
  611. },
  612. )
  613. registry = root @ Registry()
  614. resolved = registry.resolver().lookup("http://example.com/a")
  615. assert resolved.contents == {"ID": "http://example.com/a", "foo": 12}
  616. def test_lookup_anchor_with_id(self):
  617. root = ID_AND_CHILDREN.create_resource(
  618. {
  619. "ID": "http://example.com/",
  620. "anchors": {"foo": 12},
  621. },
  622. )
  623. registry = root @ Registry()
  624. resolved = registry.resolver().lookup("http://example.com/#foo")
  625. assert resolved.contents == 12
  626. def test_lookup_anchor_without_id(self):
  627. root = ID_AND_CHILDREN.create_resource({"anchors": {"foo": 12}})
  628. resolver = Registry().with_resource("urn:example", root).resolver()
  629. resolved = resolver.lookup("urn:example#foo")
  630. assert resolved.contents == 12
  631. def test_lookup_unknown_reference(self):
  632. resolver = Registry().resolver()
  633. ref = "http://example.com/does/not/exist"
  634. with pytest.raises(exceptions.Unresolvable) as e:
  635. resolver.lookup(ref)
  636. assert e.value == exceptions.Unresolvable(ref=ref)
  637. def test_lookup_non_existent_pointer(self):
  638. resource = Resource.opaque({"foo": {}})
  639. resolver = Registry({"http://example.com/1": resource}).resolver()
  640. ref = "http://example.com/1#/foo/bar"
  641. with pytest.raises(exceptions.Unresolvable) as e:
  642. resolver.lookup(ref)
  643. assert e.value == exceptions.PointerToNowhere(
  644. ref="/foo/bar",
  645. resource=resource,
  646. )
  647. assert str(e.value) == "'/foo/bar' does not exist within {'foo': {}}"
  648. def test_lookup_non_existent_pointer_to_array_index(self):
  649. resource = Resource.opaque([1, 2, 4, 8])
  650. resolver = Registry({"http://example.com/1": resource}).resolver()
  651. ref = "http://example.com/1#/10"
  652. with pytest.raises(exceptions.Unresolvable) as e:
  653. resolver.lookup(ref)
  654. assert e.value == exceptions.PointerToNowhere(
  655. ref="/10",
  656. resource=resource,
  657. )
  658. def test_lookup_pointer_to_empty_string(self):
  659. resolver = Registry().resolver_with_root(Resource.opaque({"": {}}))
  660. assert resolver.lookup("#/").contents == {}
  661. def test_lookup_non_existent_pointer_to_empty_string(self):
  662. resource = Resource.opaque({"foo": {}})
  663. resolver = Registry().resolver_with_root(resource)
  664. with pytest.raises(
  665. exceptions.Unresolvable,
  666. match="^'/' does not exist within {'foo': {}}.*'#'",
  667. ) as e:
  668. resolver.lookup("#/")
  669. assert e.value == exceptions.PointerToNowhere(
  670. ref="/",
  671. resource=resource,
  672. )
  673. def test_lookup_non_existent_anchor(self):
  674. root = ID_AND_CHILDREN.create_resource({"anchors": {}})
  675. resolver = Registry().with_resource("urn:example", root).resolver()
  676. resolved = resolver.lookup("urn:example")
  677. assert resolved.contents == root.contents
  678. ref = "urn:example#noSuchAnchor"
  679. with pytest.raises(exceptions.Unresolvable) as e:
  680. resolver.lookup(ref)
  681. assert "'noSuchAnchor' does not exist" in str(e.value)
  682. assert e.value == exceptions.NoSuchAnchor(
  683. ref="urn:example",
  684. resource=root,
  685. anchor="noSuchAnchor",
  686. )
  687. def test_lookup_invalid_JSON_pointerish_anchor(self):
  688. resolver = Registry().resolver_with_root(
  689. ID_AND_CHILDREN.create_resource(
  690. {
  691. "ID": "http://example.com/",
  692. "foo": {"bar": 12},
  693. },
  694. ),
  695. )
  696. valid = resolver.lookup("#/foo/bar")
  697. assert valid.contents == 12
  698. with pytest.raises(exceptions.InvalidAnchor) as e:
  699. resolver.lookup("#foo/bar")
  700. assert " '#/foo/bar'" in str(e.value)
  701. def test_lookup_retrieved_resource(self):
  702. resource = Resource.opaque(contents={"foo": "baz"})
  703. resolver = Registry(retrieve=lambda uri: resource).resolver()
  704. resolved = resolver.lookup("http://example.com/")
  705. assert resolved.contents == resource.contents
  706. def test_lookup_failed_retrieved_resource(self):
  707. """
  708. Unretrievable exceptions are also wrapped in Unresolvable.
  709. """
  710. uri = "http://example.com/"
  711. registry = Registry(retrieve=blow_up)
  712. with pytest.raises(exceptions.Unretrievable):
  713. registry.get_or_retrieve(uri)
  714. resolver = registry.resolver()
  715. with pytest.raises(exceptions.Unresolvable):
  716. resolver.lookup(uri)
  717. def test_repeated_lookup_from_retrieved_resource(self):
  718. """
  719. A (custom-)retrieved resource is added to the registry returned by
  720. looking it up.
  721. """
  722. resource = Resource.opaque(contents={"foo": "baz"})
  723. once = [resource]
  724. def retrieve(uri):
  725. return once.pop()
  726. resolver = Registry(retrieve=retrieve).resolver()
  727. resolved = resolver.lookup("http://example.com/")
  728. assert resolved.contents == resource.contents
  729. resolved = resolved.resolver.lookup("http://example.com/")
  730. assert resolved.contents == resource.contents
  731. def test_repeated_anchor_lookup_from_retrieved_resource(self):
  732. resource = Resource.opaque(contents={"foo": "baz"})
  733. once = [resource]
  734. def retrieve(uri):
  735. return once.pop()
  736. resolver = Registry(retrieve=retrieve).resolver()
  737. resolved = resolver.lookup("http://example.com/")
  738. assert resolved.contents == resource.contents
  739. resolved = resolved.resolver.lookup("#")
  740. assert resolved.contents == resource.contents
  741. # FIXME: The tests below aren't really representable in the current
  742. # suite, though we should probably think of ways to do so.
  743. def test_in_subresource(self):
  744. root = ID_AND_CHILDREN.create_resource(
  745. {
  746. "ID": "http://example.com/",
  747. "children": [
  748. {
  749. "ID": "child/",
  750. "children": [{"ID": "grandchild"}],
  751. },
  752. ],
  753. },
  754. )
  755. registry = root @ Registry()
  756. resolver = registry.resolver()
  757. first = resolver.lookup("http://example.com/")
  758. assert first.contents == root.contents
  759. with pytest.raises(exceptions.Unresolvable):
  760. first.resolver.lookup("grandchild")
  761. sub = first.resolver.in_subresource(
  762. ID_AND_CHILDREN.create_resource(first.contents["children"][0]),
  763. )
  764. second = sub.lookup("grandchild")
  765. assert second.contents == {"ID": "grandchild"}
  766. def test_in_pointer_subresource(self):
  767. root = ID_AND_CHILDREN.create_resource(
  768. {
  769. "ID": "http://example.com/",
  770. "children": [
  771. {
  772. "ID": "child/",
  773. "children": [{"ID": "grandchild"}],
  774. },
  775. ],
  776. },
  777. )
  778. registry = root @ Registry()
  779. resolver = registry.resolver()
  780. first = resolver.lookup("http://example.com/")
  781. assert first.contents == root.contents
  782. with pytest.raises(exceptions.Unresolvable):
  783. first.resolver.lookup("grandchild")
  784. second = first.resolver.lookup("#/children/0")
  785. third = second.resolver.lookup("grandchild")
  786. assert third.contents == {"ID": "grandchild"}
  787. def test_dynamic_scope(self):
  788. one = ID_AND_CHILDREN.create_resource(
  789. {
  790. "ID": "http://example.com/",
  791. "children": [
  792. {
  793. "ID": "child/",
  794. "children": [{"ID": "grandchild"}],
  795. },
  796. ],
  797. },
  798. )
  799. two = ID_AND_CHILDREN.create_resource(
  800. {
  801. "ID": "http://example.com/two",
  802. "children": [{"ID": "two-child/"}],
  803. },
  804. )
  805. registry = [one, two] @ Registry()
  806. resolver = registry.resolver()
  807. first = resolver.lookup("http://example.com/")
  808. second = first.resolver.lookup("#/children/0")
  809. third = second.resolver.lookup("grandchild")
  810. fourth = third.resolver.lookup("http://example.com/two")
  811. assert list(fourth.resolver.dynamic_scope()) == [
  812. ("http://example.com/child/grandchild", fourth.resolver._registry),
  813. ("http://example.com/child/", fourth.resolver._registry),
  814. ("http://example.com/", fourth.resolver._registry),
  815. ]
  816. assert list(third.resolver.dynamic_scope()) == [
  817. ("http://example.com/child/", third.resolver._registry),
  818. ("http://example.com/", third.resolver._registry),
  819. ]
  820. assert list(second.resolver.dynamic_scope()) == [
  821. ("http://example.com/", second.resolver._registry),
  822. ]
  823. assert list(first.resolver.dynamic_scope()) == []
  824. class TestSpecification:
  825. def test_create_resource(self):
  826. specification = Specification(
  827. name="",
  828. id_of=lambda contents: "urn:fixedID",
  829. subresources_of=lambda contents: [],
  830. anchors_in=lambda specification, contents: [],
  831. maybe_in_subresource=(
  832. lambda segments, resolver, subresource: resolver
  833. ),
  834. )
  835. resource = specification.create_resource(contents={"foo": "baz"})
  836. assert resource == Resource(
  837. contents={"foo": "baz"},
  838. specification=specification,
  839. )
  840. assert resource.id() == "urn:fixedID"
  841. def test_detect_from_json_schema(self):
  842. schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
  843. specification = Specification.detect(schema)
  844. assert specification == DRAFT202012
  845. def test_detect_with_no_discernible_information(self):
  846. with pytest.raises(exceptions.CannotDetermineSpecification):
  847. Specification.detect({"foo": "bar"})
  848. def test_detect_with_non_URI_schema(self):
  849. with pytest.raises(exceptions.CannotDetermineSpecification):
  850. Specification.detect({"$schema": 37})
  851. def test_detect_with_no_discernible_information_and_default(self):
  852. specification = Specification.OPAQUE.detect({"foo": "bar"})
  853. assert specification is Specification.OPAQUE
  854. def test_detect_unneeded_default(self):
  855. schema = {"$schema": "https://json-schema.org/draft/2020-12/schema"}
  856. specification = Specification.OPAQUE.detect(schema)
  857. assert specification == DRAFT202012
  858. def test_non_mapping_detect(self):
  859. with pytest.raises(exceptions.CannotDetermineSpecification):
  860. Specification.detect(True)
  861. def test_non_mapping_detect_with_default(self):
  862. specification = ID_AND_CHILDREN.detect(True)
  863. assert specification is ID_AND_CHILDREN
  864. def test_detect_with_fallback(self):
  865. specification = Specification.OPAQUE.detect({"foo": "bar"})
  866. assert specification is Specification.OPAQUE
  867. def test_repr(self):
  868. assert (
  869. repr(ID_AND_CHILDREN) == "<Specification name='id-and-children'>"
  870. )
  871. class TestOpaqueSpecification:
  872. THINGS = [{"foo": "bar"}, True, 37, "foo", object()]
  873. @pytest.mark.parametrize("thing", THINGS)
  874. def test_no_id(self, thing):
  875. """
  876. An arbitrary thing has no ID.
  877. """
  878. assert Specification.OPAQUE.id_of(thing) is None
  879. @pytest.mark.parametrize("thing", THINGS)
  880. def test_no_subresources(self, thing):
  881. """
  882. An arbitrary thing has no subresources.
  883. """
  884. assert list(Specification.OPAQUE.subresources_of(thing)) == []
  885. @pytest.mark.parametrize("thing", THINGS)
  886. def test_no_anchors(self, thing):
  887. """
  888. An arbitrary thing has no anchors.
  889. """
  890. assert list(Specification.OPAQUE.anchors_in(thing)) == []
  891. @pytest.mark.parametrize(
  892. "cls",
  893. [Anchor, Registry, Resource, Specification, exceptions.PointerToNowhere],
  894. )
  895. def test_nonsubclassable(cls):
  896. with pytest.raises(Exception, match="(?i)subclassing"):
  897. class Boom(cls): # pragma: no cover
  898. pass