1use std::{
2 collections::{hash_map::Entry, HashSet, VecDeque},
3 hash::{Hash, Hasher},
4 pin::Pin,
5 sync::Arc,
6};
7
8use ahash::{AHashMap, AHashSet, AHasher};
9use fluent_uri::Uri;
10use once_cell::sync::Lazy;
11use serde_json::Value;
12
13use crate::{
14 anchors::{AnchorKey, AnchorKeyRef},
15 cache::{SharedUriCache, UriCache},
16 hasher::BuildNoHashHasher,
17 list::List,
18 meta::{self, metas_for_draft},
19 resource::{unescape_segment, InnerResourcePtr, JsonSchemaResource},
20 uri,
21 vocabularies::{self, VocabularySet},
22 Anchor, DefaultRetriever, Draft, Error, Resolver, Resource, ResourceRef, Retrieve,
23};
24
25#[derive(Debug)]
27pub(crate) enum ValueWrapper {
28 Owned(Value),
29 StaticRef(&'static Value),
30}
31
32impl AsRef<Value> for ValueWrapper {
33 fn as_ref(&self) -> &Value {
34 match self {
35 ValueWrapper::Owned(value) => value,
36 ValueWrapper::StaticRef(value) => value,
37 }
38 }
39}
40
41type DocumentStore = AHashMap<Arc<Uri<String>>, Pin<Arc<ValueWrapper>>>;
44type ResourceMap = AHashMap<Arc<Uri<String>>, InnerResourcePtr>;
45
46pub static SPECIFICATIONS: Lazy<Registry> =
48 Lazy::new(|| Registry::build_from_meta_schemas(meta::META_SCHEMAS_ALL.as_slice()));
49
50#[derive(Debug)]
146pub struct Registry {
147 documents: DocumentStore,
148 pub(crate) resources: ResourceMap,
149 anchors: AHashMap<AnchorKey, Anchor>,
150 resolution_cache: SharedUriCache,
151}
152
153impl Clone for Registry {
154 fn clone(&self) -> Self {
155 Self {
156 documents: self.documents.clone(),
157 resources: self.resources.clone(),
158 anchors: self.anchors.clone(),
159 resolution_cache: self.resolution_cache.clone(),
160 }
161 }
162}
163
164pub struct RegistryOptions<R> {
166 retriever: R,
167 draft: Draft,
168}
169
170impl<R> RegistryOptions<R> {
171 #[must_use]
173 pub fn draft(mut self, draft: Draft) -> Self {
174 self.draft = draft;
175 self
176 }
177}
178
179impl RegistryOptions<Arc<dyn Retrieve>> {
180 #[must_use]
182 pub fn new() -> Self {
183 Self {
184 retriever: Arc::new(DefaultRetriever),
185 draft: Draft::default(),
186 }
187 }
188 #[must_use]
190 pub fn retriever(mut self, retriever: impl IntoRetriever) -> Self {
191 self.retriever = retriever.into_retriever();
192 self
193 }
194 #[cfg(feature = "retrieve-async")]
196 #[must_use]
197 pub fn async_retriever(
198 self,
199 retriever: impl IntoAsyncRetriever,
200 ) -> RegistryOptions<Arc<dyn crate::AsyncRetrieve>> {
201 RegistryOptions {
202 retriever: retriever.into_retriever(),
203 draft: self.draft,
204 }
205 }
206 pub fn build(
214 self,
215 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
216 ) -> Result<Registry, Error> {
217 Registry::try_from_resources_impl(pairs, &*self.retriever, self.draft)
218 }
219}
220
221#[cfg(feature = "retrieve-async")]
222impl RegistryOptions<Arc<dyn crate::AsyncRetrieve>> {
223 pub async fn build(
231 self,
232 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
233 ) -> Result<Registry, Error> {
234 Registry::try_from_resources_async_impl(pairs, &*self.retriever, self.draft).await
235 }
236}
237
238pub trait IntoRetriever {
239 fn into_retriever(self) -> Arc<dyn Retrieve>;
240}
241
242impl<T: Retrieve + 'static> IntoRetriever for T {
243 fn into_retriever(self) -> Arc<dyn Retrieve> {
244 Arc::new(self)
245 }
246}
247
248impl IntoRetriever for Arc<dyn Retrieve> {
249 fn into_retriever(self) -> Arc<dyn Retrieve> {
250 self
251 }
252}
253
254#[cfg(feature = "retrieve-async")]
255pub trait IntoAsyncRetriever {
256 fn into_retriever(self) -> Arc<dyn crate::AsyncRetrieve>;
257}
258
259#[cfg(feature = "retrieve-async")]
260impl<T: crate::AsyncRetrieve + 'static> IntoAsyncRetriever for T {
261 fn into_retriever(self) -> Arc<dyn crate::AsyncRetrieve> {
262 Arc::new(self)
263 }
264}
265
266#[cfg(feature = "retrieve-async")]
267impl IntoAsyncRetriever for Arc<dyn crate::AsyncRetrieve> {
268 fn into_retriever(self) -> Arc<dyn crate::AsyncRetrieve> {
269 self
270 }
271}
272
273impl Default for RegistryOptions<Arc<dyn Retrieve>> {
274 fn default() -> Self {
275 Self::new()
276 }
277}
278
279impl Registry {
280 #[must_use]
282 pub fn options() -> RegistryOptions<Arc<dyn Retrieve>> {
283 RegistryOptions::new()
284 }
285 pub fn try_new(uri: impl AsRef<str>, resource: Resource) -> Result<Self, Error> {
296 Self::try_new_impl(uri, resource, &DefaultRetriever, Draft::default())
297 }
298 pub fn try_from_resources(
308 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
309 ) -> Result<Self, Error> {
310 Self::try_from_resources_impl(pairs, &DefaultRetriever, Draft::default())
311 }
312 fn try_new_impl(
313 uri: impl AsRef<str>,
314 resource: Resource,
315 retriever: &dyn Retrieve,
316 draft: Draft,
317 ) -> Result<Self, Error> {
318 Self::try_from_resources_impl([(uri, resource)], retriever, draft)
319 }
320 fn try_from_resources_impl(
321 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
322 retriever: &dyn Retrieve,
323 draft: Draft,
324 ) -> Result<Self, Error> {
325 let mut documents = AHashMap::new();
326 let mut resources = ResourceMap::new();
327 let mut anchors = AHashMap::new();
328 let mut resolution_cache = UriCache::new();
329 process_resources(
330 pairs,
331 retriever,
332 &mut documents,
333 &mut resources,
334 &mut anchors,
335 &mut resolution_cache,
336 draft,
337 )?;
338 Ok(Registry {
339 documents,
340 resources,
341 anchors,
342 resolution_cache: resolution_cache.into_shared(),
343 })
344 }
345 #[cfg(feature = "retrieve-async")]
355 async fn try_from_resources_async_impl(
356 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
357 retriever: &dyn crate::AsyncRetrieve,
358 draft: Draft,
359 ) -> Result<Self, Error> {
360 let mut documents = AHashMap::new();
361 let mut resources = ResourceMap::new();
362 let mut anchors = AHashMap::new();
363 let mut resolution_cache = UriCache::new();
364
365 process_resources_async(
366 pairs,
367 retriever,
368 &mut documents,
369 &mut resources,
370 &mut anchors,
371 &mut resolution_cache,
372 draft,
373 )
374 .await?;
375
376 Ok(Registry {
377 documents,
378 resources,
379 anchors,
380 resolution_cache: resolution_cache.into_shared(),
381 })
382 }
383 pub fn try_with_resource(
389 self,
390 uri: impl AsRef<str>,
391 resource: Resource,
392 ) -> Result<Registry, Error> {
393 let draft = resource.draft();
394 self.try_with_resources([(uri, resource)], draft)
395 }
396 pub fn try_with_resources(
402 self,
403 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
404 draft: Draft,
405 ) -> Result<Registry, Error> {
406 self.try_with_resources_and_retriever(pairs, &DefaultRetriever, draft)
407 }
408 pub fn try_with_resources_and_retriever(
414 self,
415 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
416 retriever: &dyn Retrieve,
417 draft: Draft,
418 ) -> Result<Registry, Error> {
419 let mut documents = self.documents;
420 let mut resources = self.resources;
421 let mut anchors = self.anchors;
422 let mut resolution_cache = self.resolution_cache.into_local();
423 process_resources(
424 pairs,
425 retriever,
426 &mut documents,
427 &mut resources,
428 &mut anchors,
429 &mut resolution_cache,
430 draft,
431 )?;
432 Ok(Registry {
433 documents,
434 resources,
435 anchors,
436 resolution_cache: resolution_cache.into_shared(),
437 })
438 }
439 #[cfg(feature = "retrieve-async")]
445 pub async fn try_with_resources_and_retriever_async(
446 self,
447 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
448 retriever: &dyn crate::AsyncRetrieve,
449 draft: Draft,
450 ) -> Result<Registry, Error> {
451 let mut documents = self.documents;
452 let mut resources = self.resources;
453 let mut anchors = self.anchors;
454 let mut resolution_cache = self.resolution_cache.into_local();
455 process_resources_async(
456 pairs,
457 retriever,
458 &mut documents,
459 &mut resources,
460 &mut anchors,
461 &mut resolution_cache,
462 draft,
463 )
464 .await?;
465 Ok(Registry {
466 documents,
467 resources,
468 anchors,
469 resolution_cache: resolution_cache.into_shared(),
470 })
471 }
472 pub fn try_resolver(&self, base_uri: &str) -> Result<Resolver<'_>, Error> {
478 let base = uri::from_str(base_uri)?;
479 Ok(self.resolver(base))
480 }
481 #[must_use]
483 pub fn resolver(&self, base_uri: Uri<String>) -> Resolver<'_> {
484 Resolver::new(self, Arc::new(base_uri))
485 }
486 #[must_use]
487 pub fn resolver_from_raw_parts(
488 &self,
489 base_uri: Arc<Uri<String>>,
490 scopes: List<Uri<String>>,
491 ) -> Resolver<'_> {
492 Resolver::from_parts(self, base_uri, scopes)
493 }
494 pub(crate) fn anchor<'a>(&self, uri: &'a Uri<String>, name: &'a str) -> Result<&Anchor, Error> {
495 let key = AnchorKeyRef::new(uri, name);
496 if let Some(value) = self.anchors.get(key.borrow_dyn()) {
497 return Ok(value);
498 }
499 let resource = &self.resources[uri];
500 if let Some(id) = resource.id() {
501 let uri = uri::from_str(id)?;
502 let key = AnchorKeyRef::new(&uri, name);
503 if let Some(value) = self.anchors.get(key.borrow_dyn()) {
504 return Ok(value);
505 }
506 }
507 if name.contains('/') {
508 Err(Error::invalid_anchor(name.to_string()))
509 } else {
510 Err(Error::no_such_anchor(name.to_string()))
511 }
512 }
513 pub fn resolve_against(&self, base: &Uri<&str>, uri: &str) -> Result<Arc<Uri<String>>, Error> {
519 self.resolution_cache.resolve_against(base, uri)
520 }
521 #[must_use]
523 pub fn find_vocabularies(&self, draft: Draft, contents: &Value) -> VocabularySet {
524 match draft.detect(contents) {
525 Ok(draft) => draft.default_vocabularies(),
526 Err(Error::UnknownSpecification { specification }) => {
527 if let Ok(Some(resource)) =
529 uri::from_str(&specification).map(|uri| self.resources.get(&uri))
530 {
531 if let Ok(Some(vocabularies)) = vocabularies::find(resource.contents()) {
532 return vocabularies;
533 }
534 }
535 draft.default_vocabularies()
536 }
537 _ => unreachable!(),
538 }
539 }
540
541 pub(crate) fn build_from_meta_schemas(schemas: &[(&'static str, &'static Value)]) -> Self {
543 let schemas_count = schemas.len();
544 let pairs = schemas.iter().map(|(uri, schema)| {
545 (
546 uri,
547 ResourceRef::from_contents(schema).expect("Invalid resource"),
548 )
549 });
550
551 let mut documents = DocumentStore::with_capacity(schemas_count);
552 let mut resources = ResourceMap::with_capacity(schemas_count);
553
554 let mut anchors = AHashMap::with_capacity(8);
558 let mut resolution_cache = UriCache::with_capacity(35);
559
560 process_meta_schemas(
561 pairs,
562 &mut documents,
563 &mut resources,
564 &mut anchors,
565 &mut resolution_cache,
566 )
567 .expect("Failed to process meta schemas");
568
569 Self {
570 documents,
571 resources,
572 anchors,
573 resolution_cache: resolution_cache.into_shared(),
574 }
575 }
576}
577
578fn process_meta_schemas(
579 pairs: impl IntoIterator<Item = (impl AsRef<str>, ResourceRef<'static>)>,
580 documents: &mut DocumentStore,
581 resources: &mut ResourceMap,
582 anchors: &mut AHashMap<AnchorKey, Anchor>,
583 resolution_cache: &mut UriCache,
584) -> Result<(), Error> {
585 let mut queue = VecDeque::with_capacity(32);
586
587 for (uri, resource) in pairs {
588 let uri = uri::from_str(uri.as_ref().trim_end_matches('#'))?;
589 let key = Arc::new(uri);
590 let contents: &'static Value = resource.contents();
591 let wrapped_value = Arc::pin(ValueWrapper::StaticRef(contents));
592 let resource = InnerResourcePtr::new((*wrapped_value).as_ref(), resource.draft());
593 documents.insert(Arc::clone(&key), wrapped_value);
594 resources.insert(Arc::clone(&key), resource.clone());
595 queue.push_back((key, resource));
596 }
597
598 while let Some((mut base, resource)) = queue.pop_front() {
600 if let Some(id) = resource.id() {
601 base = resolution_cache.resolve_against(&base.borrow(), id)?;
602 resources.insert(base.clone(), resource.clone());
603 }
604
605 for anchor in resource.anchors() {
607 anchors.insert(AnchorKey::new(base.clone(), anchor.name()), anchor);
608 }
609
610 for contents in resource.draft().subresources_of(resource.contents()) {
612 let subresource = InnerResourcePtr::new(contents, resource.draft());
613 queue.push_back((base.clone(), subresource));
614 }
615 }
616 Ok(())
617}
618
619struct ProcessingState {
620 queue: VecDeque<(Arc<Uri<String>>, InnerResourcePtr)>,
621 seen: HashSet<u64, BuildNoHashHasher>,
622 external: AHashSet<(String, Uri<String>)>,
623 scratch: String,
624 refers_metaschemas: bool,
625}
626
627impl ProcessingState {
628 fn new() -> Self {
629 Self {
630 queue: VecDeque::with_capacity(32),
631 seen: HashSet::with_hasher(BuildNoHashHasher::default()),
632 external: AHashSet::new(),
633 scratch: String::new(),
634 refers_metaschemas: false,
635 }
636 }
637}
638
639fn process_input_resources(
640 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
641 documents: &mut DocumentStore,
642 resources: &mut ResourceMap,
643 state: &mut ProcessingState,
644) -> Result<(), Error> {
645 for (uri, resource) in pairs {
646 let uri = uri::from_str(uri.as_ref().trim_end_matches('#'))?;
647 let key = Arc::new(uri);
648 match documents.entry(Arc::clone(&key)) {
649 Entry::Occupied(_) => {}
650 Entry::Vacant(entry) => {
651 let (draft, contents) = resource.into_inner();
652 let wrapped_value = Arc::pin(ValueWrapper::Owned(contents));
653 let resource = InnerResourcePtr::new((*wrapped_value).as_ref(), draft);
654 resources.insert(Arc::clone(&key), resource.clone());
655 state.queue.push_back((key, resource));
656 entry.insert(wrapped_value);
657 }
658 }
659 }
660 Ok(())
661}
662
663fn process_queue(
664 state: &mut ProcessingState,
665 resources: &mut ResourceMap,
666 anchors: &mut AHashMap<AnchorKey, Anchor>,
667 resolution_cache: &mut UriCache,
668) -> Result<(), Error> {
669 while let Some((mut base, resource)) = state.queue.pop_front() {
670 if let Some(id) = resource.id() {
671 base = resolution_cache.resolve_against(&base.borrow(), id)?;
672 resources.insert(base.clone(), resource.clone());
673 }
674
675 for anchor in resource.anchors() {
676 anchors.insert(AnchorKey::new(base.clone(), anchor.name()), anchor);
677 }
678
679 collect_external_resources(
680 &base,
681 resource.contents(),
682 &mut state.external,
683 &mut state.seen,
684 resolution_cache,
685 &mut state.scratch,
686 &mut state.refers_metaschemas,
687 )?;
688
689 for contents in resource.draft().subresources_of(resource.contents()) {
690 let subresource = InnerResourcePtr::new(contents, resource.draft());
691 state.queue.push_back((base.clone(), subresource));
692 }
693 }
694 Ok(())
695}
696
697fn handle_fragment(
698 uri: &Uri<String>,
699 resource: &InnerResourcePtr,
700 key: &Arc<Uri<String>>,
701 default_draft: Draft,
702 queue: &mut VecDeque<(Arc<Uri<String>>, InnerResourcePtr)>,
703) -> Result<(), Error> {
704 if let Some(fragment) = uri.fragment() {
705 if let Some(resolved) = pointer(resource.contents(), fragment.as_str()) {
706 let draft = default_draft.detect(resolved)?;
707 let contents = std::ptr::addr_of!(*resolved);
708 let resource = InnerResourcePtr::new(contents, draft);
709 queue.push_back((Arc::clone(key), resource));
710 }
711 }
712 Ok(())
713}
714
715fn handle_metaschemas(
716 refers_metaschemas: bool,
717 resources: &mut ResourceMap,
718 anchors: &mut AHashMap<AnchorKey, Anchor>,
719 draft_version: Draft,
720) {
721 if refers_metaschemas {
722 let schemas = metas_for_draft(draft_version);
723 let draft_registry = Registry::build_from_meta_schemas(schemas);
724 resources.reserve(draft_registry.resources.len());
725 for (key, resource) in draft_registry.resources {
726 resources.insert(key, resource.clone());
727 }
728 anchors.reserve(draft_registry.anchors.len());
729 for (key, anchor) in draft_registry.anchors {
730 anchors.insert(key, anchor);
731 }
732 }
733}
734
735fn create_resource(
736 retrieved: Value,
737 fragmentless: Uri<String>,
738 default_draft: Draft,
739 documents: &mut DocumentStore,
740 resources: &mut ResourceMap,
741) -> Result<(Arc<Uri<String>>, InnerResourcePtr), Error> {
742 let draft = default_draft.detect(&retrieved)?;
743 let wrapped_value = Arc::pin(ValueWrapper::Owned(retrieved));
744 let resource = InnerResourcePtr::new((*wrapped_value).as_ref(), draft);
745 let key = Arc::new(fragmentless);
746 documents.insert(Arc::clone(&key), wrapped_value);
747 resources.insert(Arc::clone(&key), resource.clone());
748 Ok((key, resource))
749}
750
751fn process_resources(
752 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
753 retriever: &dyn Retrieve,
754 documents: &mut DocumentStore,
755 resources: &mut ResourceMap,
756 anchors: &mut AHashMap<AnchorKey, Anchor>,
757 resolution_cache: &mut UriCache,
758 default_draft: Draft,
759) -> Result<(), Error> {
760 let mut state = ProcessingState::new();
761 process_input_resources(pairs, documents, resources, &mut state)?;
762
763 loop {
764 if state.queue.is_empty() && state.external.is_empty() {
765 break;
766 }
767
768 process_queue(&mut state, resources, anchors, resolution_cache)?;
769
770 for (original, uri) in state.external.drain() {
772 let mut fragmentless = uri.clone();
773 fragmentless.set_fragment(None);
774 if !resources.contains_key(&fragmentless) {
775 let retrieved = match retriever.retrieve(&fragmentless) {
776 Ok(retrieved) => retrieved,
777 Err(error) => {
778 return if uri.scheme().as_str() == "json-schema" {
779 Err(Error::unretrievable(
780 original,
781 "No base URI is available".into(),
782 ))
783 } else {
784 Err(Error::unretrievable(fragmentless.as_str(), error))
785 }
786 }
787 };
788
789 let (key, resource) =
790 create_resource(retrieved, fragmentless, default_draft, documents, resources)?;
791
792 handle_fragment(&uri, &resource, &key, default_draft, &mut state.queue)?;
793
794 state.queue.push_back((key, resource));
795 }
796 }
797 }
798
799 handle_metaschemas(state.refers_metaschemas, resources, anchors, default_draft);
800
801 Ok(())
802}
803
804#[cfg(feature = "retrieve-async")]
805async fn process_resources_async(
806 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
807 retriever: &dyn crate::AsyncRetrieve,
808 documents: &mut DocumentStore,
809 resources: &mut ResourceMap,
810 anchors: &mut AHashMap<AnchorKey, Anchor>,
811 resolution_cache: &mut UriCache,
812 default_draft: Draft,
813) -> Result<(), Error> {
814 let mut state = ProcessingState::new();
815 process_input_resources(pairs, documents, resources, &mut state)?;
816
817 loop {
818 if state.queue.is_empty() && state.external.is_empty() {
819 break;
820 }
821
822 process_queue(&mut state, resources, anchors, resolution_cache)?;
823
824 if !state.external.is_empty() {
825 let data = state
826 .external
827 .drain()
828 .filter_map(|(original, uri)| {
829 let mut fragmentless = uri.clone();
830 fragmentless.set_fragment(None);
831 if resources.contains_key(&fragmentless) {
832 None
833 } else {
834 Some((original, uri, fragmentless))
835 }
836 })
837 .collect::<Vec<_>>();
838
839 let results = {
840 let futures = data
841 .iter()
842 .map(|(_, _, fragmentless)| retriever.retrieve(fragmentless));
843 futures::future::join_all(futures).await
844 };
845
846 for ((original, uri, fragmentless), result) in data.iter().zip(results) {
847 let retrieved = match result {
848 Ok(retrieved) => retrieved,
849 Err(error) => {
850 return if uri.scheme().as_str() == "json-schema" {
851 Err(Error::unretrievable(
852 original,
853 "No base URI is available".into(),
854 ))
855 } else {
856 Err(Error::unretrievable(fragmentless.as_str(), error))
857 }
858 }
859 };
860
861 let (key, resource) = create_resource(
862 retrieved,
863 fragmentless.clone(),
864 default_draft,
865 documents,
866 resources,
867 )?;
868
869 handle_fragment(uri, &resource, &key, default_draft, &mut state.queue)?;
870
871 state.queue.push_back((key, resource));
872 }
873 }
874 }
875
876 handle_metaschemas(state.refers_metaschemas, resources, anchors, default_draft);
877
878 Ok(())
879}
880
881fn collect_external_resources(
882 base: &Uri<String>,
883 contents: &Value,
884 collected: &mut AHashSet<(String, Uri<String>)>,
885 seen: &mut HashSet<u64, BuildNoHashHasher>,
886 resolution_cache: &mut UriCache,
887 scratch: &mut String,
888 refers_metaschemas: &mut bool,
889) -> Result<(), Error> {
890 if base.scheme().as_str() == "urn" {
892 return Ok(());
893 }
894
895 macro_rules! on_reference {
896 ($reference:expr, $key:literal) => {
897 if $reference.starts_with("https://json-schema.org/draft/")
899 || $reference.starts_with("http://json-schema.org/draft-")
900 || base.as_str().starts_with("https://json-schema.org/draft/")
901 {
902 if $key == "$ref" {
903 *refers_metaschemas = true;
904 }
905 } else if $reference != "#" {
906 let mut hasher = AHasher::default();
907 (base.as_str(), $reference).hash(&mut hasher);
908 let hash = hasher.finish();
909 if seen.insert(hash) {
910 if $reference.starts_with('#') {
912 if let Some(referenced) =
913 pointer(contents, $reference.trim_start_matches('#'))
914 {
915 collect_external_resources(
916 base,
917 referenced,
918 collected,
919 seen,
920 resolution_cache,
921 scratch,
922 refers_metaschemas,
923 )?;
924 }
925 } else {
926 let resolved = if base.has_fragment() {
927 let mut base_without_fragment = base.clone();
928 base_without_fragment.set_fragment(None);
929
930 let (path, fragment) = match $reference.split_once('#') {
931 Some((path, fragment)) => (path, Some(fragment)),
932 None => ($reference, None),
933 };
934
935 let mut resolved = (*resolution_cache
936 .resolve_against(&base_without_fragment.borrow(), path)?)
937 .clone();
938 if let Some(fragment) = fragment {
940 if let Some(encoded) = uri::EncodedString::new(fragment) {
943 resolved = resolved.with_fragment(Some(encoded));
944 } else {
945 uri::encode_to(fragment, scratch);
946 resolved = resolved.with_fragment(Some(
947 uri::EncodedString::new_or_panic(scratch),
948 ));
949 scratch.clear();
950 }
951 }
952 resolved
953 } else {
954 (*resolution_cache.resolve_against(&base.borrow(), $reference)?).clone()
955 };
956
957 collected.insert(($reference.to_string(), resolved));
958 }
959 }
960 }
961 };
962 }
963
964 if let Some(object) = contents.as_object() {
965 if object.len() < 3 {
966 for (key, value) in object {
967 if key == "$ref" {
968 if let Some(reference) = value.as_str() {
969 on_reference!(reference, "$ref");
970 }
971 } else if key == "$schema" {
972 if let Some(reference) = value.as_str() {
973 on_reference!(reference, "$schema");
974 }
975 }
976 }
977 } else {
978 if let Some(reference) = object.get("$ref").and_then(Value::as_str) {
979 on_reference!(reference, "$ref");
980 }
981 if let Some(reference) = object.get("$schema").and_then(Value::as_str) {
982 on_reference!(reference, "$schema");
983 }
984 }
985 }
986 Ok(())
987}
988
989pub fn pointer<'a>(document: &'a Value, pointer: &str) -> Option<&'a Value> {
993 if pointer.is_empty() {
994 return Some(document);
995 }
996 if !pointer.starts_with('/') {
997 return None;
998 }
999 pointer.split('/').skip(1).map(unescape_segment).try_fold(
1000 document,
1001 |target, token| match target {
1002 Value::Object(map) => map.get(&*token),
1003 Value::Array(list) => parse_index(&token).and_then(|x| list.get(x)),
1004 _ => None,
1005 },
1006 )
1007}
1008
1009#[must_use]
1011pub fn parse_index(s: &str) -> Option<usize> {
1012 if s.starts_with('+') || (s.starts_with('0') && s.len() != 1) {
1013 return None;
1014 }
1015 s.parse().ok()
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020 use std::error::Error as _;
1021
1022 use ahash::AHashMap;
1023 use fluent_uri::Uri;
1024 use serde_json::{json, Value};
1025 use test_case::test_case;
1026
1027 use crate::{uri::from_str, Draft, Registry, Resource, Retrieve};
1028
1029 use super::{pointer, RegistryOptions, SPECIFICATIONS};
1030
1031 #[test]
1032 fn test_empty_pointer() {
1033 let document = json!({});
1034 assert_eq!(pointer(&document, ""), Some(&document));
1035 }
1036
1037 #[test]
1038 fn test_invalid_uri_on_registry_creation() {
1039 let schema = Draft::Draft202012.create_resource(json!({}));
1040 let result = Registry::try_new(":/example.com", schema);
1041 let error = result.expect_err("Should fail");
1042
1043 assert_eq!(
1044 error.to_string(),
1045 "Invalid URI reference ':/example.com': unexpected character at index 0"
1046 );
1047 let source_error = error.source().expect("Should have a source");
1048 let inner_source = source_error.source().expect("Should have a source");
1049 assert_eq!(inner_source.to_string(), "unexpected character at index 0");
1050 }
1051
1052 #[test]
1053 fn test_lookup_unresolvable_url() {
1054 let schema = Draft::Draft202012.create_resource(json!({
1056 "type": "object",
1057 "properties": {
1058 "foo": { "type": "string" }
1059 }
1060 }));
1061 let registry =
1062 Registry::try_new("http://example.com/schema1", schema).expect("Invalid resources");
1063
1064 let resolver = registry
1066 .try_resolver("http://example.com/non_existent_schema")
1067 .expect("Invalid base URI");
1068
1069 let result = resolver.lookup("");
1070
1071 assert_eq!(
1072 result.unwrap_err().to_string(),
1073 "Resource 'http://example.com/non_existent_schema' is not present in a registry and retrieving it failed: Retrieving external resources is not supported once the registry is populated"
1074 );
1075 }
1076
1077 #[test]
1078 fn test_relative_uri_without_base() {
1079 let schema = Draft::Draft202012.create_resource(json!({"$ref": "./virtualNetwork.json"}));
1080 let error = Registry::try_new("json-schema:///", schema).expect_err("Should fail");
1081 assert_eq!(error.to_string(), "Resource './virtualNetwork.json' is not present in a registry and retrieving it failed: No base URI is available");
1082 }
1083
1084 struct TestRetriever {
1085 schemas: AHashMap<String, Value>,
1086 }
1087
1088 impl TestRetriever {
1089 fn new(schemas: AHashMap<String, Value>) -> Self {
1090 TestRetriever { schemas }
1091 }
1092 }
1093
1094 impl Retrieve for TestRetriever {
1095 fn retrieve(
1096 &self,
1097 uri: &Uri<String>,
1098 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
1099 if let Some(value) = self.schemas.get(uri.as_str()) {
1100 Ok(value.clone())
1101 } else {
1102 Err(format!("Failed to find {uri}").into())
1103 }
1104 }
1105 }
1106
1107 fn create_test_retriever(schemas: &[(&str, Value)]) -> TestRetriever {
1108 TestRetriever::new(
1109 schemas
1110 .iter()
1111 .map(|&(k, ref v)| (k.to_string(), v.clone()))
1112 .collect(),
1113 )
1114 }
1115
1116 struct TestCase {
1117 input_resources: Vec<(&'static str, Value)>,
1118 remote_resources: Vec<(&'static str, Value)>,
1119 expected_resolved_uris: Vec<&'static str>,
1120 }
1121
1122 #[test_case(
1123 TestCase {
1124 input_resources: vec![
1125 ("http://example.com/schema1", json!({"$ref": "http://example.com/schema2"})),
1126 ],
1127 remote_resources: vec![
1128 ("http://example.com/schema2", json!({"type": "object"})),
1129 ],
1130 expected_resolved_uris: vec!["http://example.com/schema1", "http://example.com/schema2"],
1131 }
1132 ;"External ref at top")]
1133 #[test_case(
1134 TestCase {
1135 input_resources: vec![
1136 ("http://example.com/schema1", json!({
1137 "$defs": {
1138 "subschema": {"type": "string"}
1139 },
1140 "$ref": "#/$defs/subschema"
1141 })),
1142 ],
1143 remote_resources: vec![],
1144 expected_resolved_uris: vec!["http://example.com/schema1"],
1145 }
1146 ;"Internal ref at top")]
1147 #[test_case(
1148 TestCase {
1149 input_resources: vec![
1150 ("http://example.com/schema1", json!({"$ref": "http://example.com/schema2"})),
1151 ("http://example.com/schema2", json!({"type": "object"})),
1152 ],
1153 remote_resources: vec![],
1154 expected_resolved_uris: vec!["http://example.com/schema1", "http://example.com/schema2"],
1155 }
1156 ;"Ref to later resource")]
1157 #[test_case(
1158 TestCase {
1159 input_resources: vec![
1160 ("http://example.com/schema1", json!({
1161 "type": "object",
1162 "properties": {
1163 "prop1": {"$ref": "http://example.com/schema2"}
1164 }
1165 })),
1166 ],
1167 remote_resources: vec![
1168 ("http://example.com/schema2", json!({"type": "string"})),
1169 ],
1170 expected_resolved_uris: vec!["http://example.com/schema1", "http://example.com/schema2"],
1171 }
1172 ;"External ref in subresource")]
1173 #[test_case(
1174 TestCase {
1175 input_resources: vec![
1176 ("http://example.com/schema1", json!({
1177 "type": "object",
1178 "properties": {
1179 "prop1": {"$ref": "#/$defs/subschema"}
1180 },
1181 "$defs": {
1182 "subschema": {"type": "string"}
1183 }
1184 })),
1185 ],
1186 remote_resources: vec![],
1187 expected_resolved_uris: vec!["http://example.com/schema1"],
1188 }
1189 ;"Internal ref in subresource")]
1190 #[test_case(
1191 TestCase {
1192 input_resources: vec![
1193 ("file:///schemas/main.json", json!({"$ref": "file:///schemas/external.json"})),
1194 ],
1195 remote_resources: vec![
1196 ("file:///schemas/external.json", json!({"type": "object"})),
1197 ],
1198 expected_resolved_uris: vec!["file:///schemas/main.json", "file:///schemas/external.json"],
1199 }
1200 ;"File scheme: external ref at top")]
1201 #[test_case(
1202 TestCase {
1203 input_resources: vec![
1204 ("file:///schemas/main.json", json!({"$ref": "subfolder/schema.json"})),
1205 ],
1206 remote_resources: vec![
1207 ("file:///schemas/subfolder/schema.json", json!({"type": "string"})),
1208 ],
1209 expected_resolved_uris: vec!["file:///schemas/main.json", "file:///schemas/subfolder/schema.json"],
1210 }
1211 ;"File scheme: relative path ref")]
1212 #[test_case(
1213 TestCase {
1214 input_resources: vec![
1215 ("file:///schemas/main.json", json!({
1216 "type": "object",
1217 "properties": {
1218 "local": {"$ref": "local.json"},
1219 "remote": {"$ref": "http://example.com/schema"}
1220 }
1221 })),
1222 ],
1223 remote_resources: vec![
1224 ("file:///schemas/local.json", json!({"type": "string"})),
1225 ("http://example.com/schema", json!({"type": "number"})),
1226 ],
1227 expected_resolved_uris: vec![
1228 "file:///schemas/main.json",
1229 "file:///schemas/local.json",
1230 "http://example.com/schema"
1231 ],
1232 }
1233 ;"File scheme: mixing with http scheme")]
1234 #[test_case(
1235 TestCase {
1236 input_resources: vec![
1237 ("file:///C:/schemas/main.json", json!({"$ref": "/D:/other_schemas/schema.json"})),
1238 ],
1239 remote_resources: vec![
1240 ("file:///D:/other_schemas/schema.json", json!({"type": "boolean"})),
1241 ],
1242 expected_resolved_uris: vec![
1243 "file:///C:/schemas/main.json",
1244 "file:///D:/other_schemas/schema.json"
1245 ],
1246 }
1247 ;"File scheme: absolute path in Windows style")]
1248 #[test_case(
1249 TestCase {
1250 input_resources: vec![
1251 ("http://example.com/schema1", json!({"$ref": "http://example.com/schema2"})),
1252 ],
1253 remote_resources: vec![
1254 ("http://example.com/schema2", json!({"$ref": "http://example.com/schema3"})),
1255 ("http://example.com/schema3", json!({"$ref": "http://example.com/schema4"})),
1256 ("http://example.com/schema4", json!({"$ref": "http://example.com/schema5"})),
1257 ("http://example.com/schema5", json!({"type": "object"})),
1258 ],
1259 expected_resolved_uris: vec![
1260 "http://example.com/schema1",
1261 "http://example.com/schema2",
1262 "http://example.com/schema3",
1263 "http://example.com/schema4",
1264 "http://example.com/schema5",
1265 ],
1266 }
1267 ;"Four levels of external references")]
1268 #[test_case(
1269 TestCase {
1270 input_resources: vec![
1271 ("http://example.com/schema1", json!({"$ref": "http://example.com/schema2"})),
1272 ],
1273 remote_resources: vec![
1274 ("http://example.com/schema2", json!({"$ref": "http://example.com/schema3"})),
1275 ("http://example.com/schema3", json!({"$ref": "http://example.com/schema4"})),
1276 ("http://example.com/schema4", json!({"$ref": "http://example.com/schema5"})),
1277 ("http://example.com/schema5", json!({"$ref": "http://example.com/schema6"})),
1278 ("http://example.com/schema6", json!({"$ref": "http://example.com/schema1"})),
1279 ],
1280 expected_resolved_uris: vec![
1281 "http://example.com/schema1",
1282 "http://example.com/schema2",
1283 "http://example.com/schema3",
1284 "http://example.com/schema4",
1285 "http://example.com/schema5",
1286 "http://example.com/schema6",
1287 ],
1288 }
1289 ;"Five levels of external references with circular reference")]
1290 fn test_references_processing(test_case: TestCase) {
1291 let retriever = create_test_retriever(&test_case.remote_resources);
1292
1293 let input_pairs = test_case
1294 .input_resources
1295 .clone()
1296 .into_iter()
1297 .map(|(uri, value)| {
1298 (
1299 uri,
1300 Resource::from_contents(value).expect("Invalid resource"),
1301 )
1302 });
1303
1304 let registry = Registry::options()
1305 .retriever(retriever)
1306 .build(input_pairs)
1307 .expect("Invalid resources");
1308 for uri in test_case.expected_resolved_uris {
1310 let resolver = registry.try_resolver("").expect("Invalid base URI");
1311 assert!(resolver.lookup(uri).is_ok());
1312 }
1313 }
1314
1315 #[test]
1316 fn test_default_retriever_with_remote_refs() {
1317 let result = Registry::try_from_resources([(
1318 "http://example.com/schema1",
1319 Resource::from_contents(json!({"$ref": "http://example.com/schema2"}))
1320 .expect("Invalid resource"),
1321 )]);
1322 let error = result.expect_err("Should fail");
1323 assert_eq!(error.to_string(), "Resource 'http://example.com/schema2' is not present in a registry and retrieving it failed: Default retriever does not fetch resources");
1324 assert!(error.source().is_some());
1325 }
1326
1327 #[test]
1328 fn test_options() {
1329 let _registry = RegistryOptions::default()
1330 .build([(
1331 "",
1332 Resource::from_contents(json!({})).expect("Invalid resource"),
1333 )])
1334 .expect("Invalid resources");
1335 }
1336
1337 #[test]
1338 fn test_registry_with_duplicate_input_uris() {
1339 let input_resources = vec![
1340 (
1341 "http://example.com/schema",
1342 json!({
1343 "type": "object",
1344 "properties": {
1345 "foo": { "type": "string" }
1346 }
1347 }),
1348 ),
1349 (
1350 "http://example.com/schema",
1351 json!({
1352 "type": "object",
1353 "properties": {
1354 "bar": { "type": "number" }
1355 }
1356 }),
1357 ),
1358 ];
1359
1360 let result = Registry::try_from_resources(
1361 input_resources
1362 .into_iter()
1363 .map(|(uri, value)| (uri, Draft::Draft202012.create_resource(value))),
1364 );
1365
1366 assert!(
1367 result.is_ok(),
1368 "Failed to create registry with duplicate input URIs"
1369 );
1370 let registry = result.unwrap();
1371
1372 let resource = registry
1373 .resources
1374 .get(&from_str("http://example.com/schema").expect("Invalid URI"))
1375 .unwrap();
1376 let properties = resource
1377 .contents()
1378 .get("properties")
1379 .and_then(|v| v.as_object())
1380 .unwrap();
1381
1382 assert!(
1383 !properties.contains_key("bar"),
1384 "Registry should contain the earliest added schema"
1385 );
1386 assert!(
1387 properties.contains_key("foo"),
1388 "Registry should contain the overwritten schema"
1389 );
1390 }
1391
1392 #[test]
1393 fn test_resolver_debug() {
1394 let registry = SPECIFICATIONS
1395 .clone()
1396 .try_with_resource(
1397 "http://example.com",
1398 Resource::from_contents(json!({})).expect("Invalid resource"),
1399 )
1400 .expect("Invalid resource");
1401 let resolver = registry
1402 .try_resolver("http://127.0.0.1/schema")
1403 .expect("Invalid base URI");
1404 assert_eq!(
1405 format!("{resolver:?}"),
1406 "Resolver { base_uri: \"http://127.0.0.1/schema\", scopes: \"[]\" }"
1407 );
1408 }
1409
1410 #[test]
1411 fn test_try_with_resource() {
1412 let registry = SPECIFICATIONS
1413 .clone()
1414 .try_with_resource(
1415 "http://example.com",
1416 Resource::from_contents(json!({})).expect("Invalid resource"),
1417 )
1418 .expect("Invalid resource");
1419 let resolver = registry.try_resolver("").expect("Invalid base URI");
1420 let resolved = resolver
1421 .lookup("http://json-schema.org/draft-06/schema#/definitions/schemaArray")
1422 .expect("Lookup failed");
1423 assert_eq!(
1424 resolved.contents(),
1425 &json!({
1426 "type": "array",
1427 "minItems": 1,
1428 "items": { "$ref": "#" }
1429 })
1430 );
1431 }
1432
1433 #[test]
1434 fn test_invalid_reference() {
1435 let resource = Draft::Draft202012.create_resource(json!({"$schema": "$##"}));
1437 let _ = Registry::try_new("http://#/", resource);
1438 }
1439}
1440
1441#[cfg(all(test, feature = "retrieve-async"))]
1442mod async_tests {
1443 use crate::{uri, DefaultRetriever, Draft, Registry, Resource, Uri};
1444 use ahash::AHashMap;
1445 use serde_json::{json, Value};
1446 use std::error::Error;
1447
1448 struct TestAsyncRetriever {
1449 schemas: AHashMap<String, Value>,
1450 }
1451
1452 impl TestAsyncRetriever {
1453 fn with_schema(uri: impl Into<String>, schema: Value) -> Self {
1454 TestAsyncRetriever {
1455 schemas: { AHashMap::from_iter([(uri.into(), schema)]) },
1456 }
1457 }
1458 }
1459
1460 #[async_trait::async_trait]
1461 impl crate::AsyncRetrieve for TestAsyncRetriever {
1462 async fn retrieve(
1463 &self,
1464 uri: &Uri<String>,
1465 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
1466 self.schemas
1467 .get(uri.as_str())
1468 .cloned()
1469 .ok_or_else(|| "Schema not found".into())
1470 }
1471 }
1472
1473 #[tokio::test]
1474 async fn test_default_async_retriever_with_remote_refs() {
1475 let result = Registry::options()
1476 .async_retriever(DefaultRetriever)
1477 .build([(
1478 "http://example.com/schema1",
1479 Resource::from_contents(json!({"$ref": "http://example.com/schema2"}))
1480 .expect("Invalid resource"),
1481 )])
1482 .await;
1483
1484 let error = result.expect_err("Should fail");
1485 assert_eq!(error.to_string(), "Resource 'http://example.com/schema2' is not present in a registry and retrieving it failed: Default retriever does not fetch resources");
1486 assert!(error.source().is_some());
1487 }
1488
1489 #[tokio::test]
1490 async fn test_async_options() {
1491 let _registry = Registry::options()
1492 .async_retriever(DefaultRetriever)
1493 .build([("", Draft::default().create_resource(json!({})))])
1494 .await
1495 .expect("Invalid resources");
1496 }
1497
1498 #[tokio::test]
1499 async fn test_async_registry_with_duplicate_input_uris() {
1500 let input_resources = vec![
1501 (
1502 "http://example.com/schema",
1503 json!({
1504 "type": "object",
1505 "properties": {
1506 "foo": { "type": "string" }
1507 }
1508 }),
1509 ),
1510 (
1511 "http://example.com/schema",
1512 json!({
1513 "type": "object",
1514 "properties": {
1515 "bar": { "type": "number" }
1516 }
1517 }),
1518 ),
1519 ];
1520
1521 let result = Registry::options()
1522 .async_retriever(DefaultRetriever)
1523 .build(
1524 input_resources
1525 .into_iter()
1526 .map(|(uri, value)| (uri, Draft::Draft202012.create_resource(value))),
1527 )
1528 .await;
1529
1530 assert!(
1531 result.is_ok(),
1532 "Failed to create registry with duplicate input URIs"
1533 );
1534 let registry = result.unwrap();
1535
1536 let resource = registry
1537 .resources
1538 .get(&uri::from_str("http://example.com/schema").expect("Invalid URI"))
1539 .unwrap();
1540 let properties = resource
1541 .contents()
1542 .get("properties")
1543 .and_then(|v| v.as_object())
1544 .unwrap();
1545
1546 assert!(
1547 !properties.contains_key("bar"),
1548 "Registry should contain the earliest added schema"
1549 );
1550 assert!(
1551 properties.contains_key("foo"),
1552 "Registry should contain the overwritten schema"
1553 );
1554 }
1555
1556 #[tokio::test]
1557 async fn test_async_try_with_resource() {
1558 let retriever = TestAsyncRetriever::with_schema(
1559 "http://example.com/schema2",
1560 json!({"type": "object"}),
1561 );
1562
1563 let registry = Registry::options()
1564 .async_retriever(retriever)
1565 .build([(
1566 "http://example.com",
1567 Resource::from_contents(json!({"$ref": "http://example.com/schema2"}))
1568 .expect("Invalid resource"),
1569 )])
1570 .await
1571 .expect("Invalid resource");
1572
1573 let resolver = registry.try_resolver("").expect("Invalid base URI");
1574 let resolved = resolver
1575 .lookup("http://example.com/schema2")
1576 .expect("Lookup failed");
1577 assert_eq!(resolved.contents(), &json!({"type": "object"}));
1578 }
1579
1580 #[tokio::test]
1581 async fn test_async_registry_with_multiple_refs() {
1582 let retriever = TestAsyncRetriever {
1583 schemas: AHashMap::from_iter([
1584 (
1585 "http://example.com/schema2".to_string(),
1586 json!({"type": "object"}),
1587 ),
1588 (
1589 "http://example.com/schema3".to_string(),
1590 json!({"type": "string"}),
1591 ),
1592 ]),
1593 };
1594
1595 let registry = Registry::options()
1596 .async_retriever(retriever)
1597 .build([(
1598 "http://example.com/schema1",
1599 Resource::from_contents(json!({
1600 "type": "object",
1601 "properties": {
1602 "obj": {"$ref": "http://example.com/schema2"},
1603 "str": {"$ref": "http://example.com/schema3"}
1604 }
1605 }))
1606 .expect("Invalid resource"),
1607 )])
1608 .await
1609 .expect("Invalid resource");
1610
1611 let resolver = registry.try_resolver("").expect("Invalid base URI");
1612
1613 let resolved2 = resolver
1615 .lookup("http://example.com/schema2")
1616 .expect("Lookup failed");
1617 assert_eq!(resolved2.contents(), &json!({"type": "object"}));
1618
1619 let resolved3 = resolver
1620 .lookup("http://example.com/schema3")
1621 .expect("Lookup failed");
1622 assert_eq!(resolved3.contents(), &json!({"type": "string"}));
1623 }
1624
1625 #[tokio::test]
1626 async fn test_async_registry_with_nested_refs() {
1627 let retriever = TestAsyncRetriever {
1628 schemas: AHashMap::from_iter([
1629 (
1630 "http://example.com/address".to_string(),
1631 json!({
1632 "type": "object",
1633 "properties": {
1634 "street": {"type": "string"},
1635 "city": {"$ref": "http://example.com/city"}
1636 }
1637 }),
1638 ),
1639 (
1640 "http://example.com/city".to_string(),
1641 json!({
1642 "type": "string",
1643 "minLength": 1
1644 }),
1645 ),
1646 ]),
1647 };
1648
1649 let registry = Registry::options()
1650 .async_retriever(retriever)
1651 .build([(
1652 "http://example.com/person",
1653 Resource::from_contents(json!({
1654 "type": "object",
1655 "properties": {
1656 "name": {"type": "string"},
1657 "address": {"$ref": "http://example.com/address"}
1658 }
1659 }))
1660 .expect("Invalid resource"),
1661 )])
1662 .await
1663 .expect("Invalid resource");
1664
1665 let resolver = registry.try_resolver("").expect("Invalid base URI");
1666
1667 let resolved = resolver
1669 .lookup("http://example.com/city")
1670 .expect("Lookup failed");
1671 assert_eq!(
1672 resolved.contents(),
1673 &json!({"type": "string", "minLength": 1})
1674 );
1675 }
1676}