1use std::{
2 collections::{hash_map::Entry, VecDeque},
3 num::NonZeroUsize,
4 pin::Pin,
5 sync::{Arc, LazyLock},
6};
7
8use ahash::{AHashMap, AHashSet};
9use fluent_uri::{pct_enc::EStr, Uri};
10use serde_json::Value;
11
12use crate::{
13 anchors::{AnchorKey, AnchorKeyRef},
14 cache::{SharedUriCache, UriCache},
15 meta::{self, metas_for_draft},
16 resource::{unescape_segment, InnerResourcePtr, JsonSchemaResource},
17 uri,
18 vocabularies::{self, VocabularySet},
19 Anchor, DefaultRetriever, Draft, Error, Resolver, Resource, ResourceRef, Retrieve,
20};
21
22#[derive(Debug)]
24pub(crate) enum ValueWrapper {
25 Owned(Value),
26 StaticRef(&'static Value),
27}
28
29impl AsRef<Value> for ValueWrapper {
30 fn as_ref(&self) -> &Value {
31 match self {
32 ValueWrapper::Owned(value) => value,
33 ValueWrapper::StaticRef(value) => value,
34 }
35 }
36}
37
38type DocumentStore = AHashMap<Arc<Uri<String>>, Pin<Arc<ValueWrapper>>>;
41type ResourceMap = AHashMap<Arc<Uri<String>>, InnerResourcePtr>;
42
43pub static SPECIFICATIONS: LazyLock<Registry> =
45 LazyLock::new(|| Registry::build_from_meta_schemas(meta::META_SCHEMAS_ALL.as_slice()));
46
47#[derive(Debug)]
144pub struct Registry {
145 documents: DocumentStore,
146 pub(crate) resources: ResourceMap,
147 anchors: AHashMap<AnchorKey, Anchor>,
148 resolution_cache: SharedUriCache,
149}
150
151impl Clone for Registry {
152 fn clone(&self) -> Self {
153 Self {
154 documents: self.documents.clone(),
155 resources: self.resources.clone(),
156 anchors: self.anchors.clone(),
157 resolution_cache: self.resolution_cache.clone(),
158 }
159 }
160}
161
162pub struct RegistryOptions<R> {
164 retriever: R,
165 draft: Draft,
166}
167
168impl<R> RegistryOptions<R> {
169 #[must_use]
171 pub fn draft(mut self, draft: Draft) -> Self {
172 self.draft = draft;
173 self
174 }
175}
176
177impl RegistryOptions<Arc<dyn Retrieve>> {
178 #[must_use]
180 pub fn new() -> Self {
181 Self {
182 retriever: Arc::new(DefaultRetriever),
183 draft: Draft::default(),
184 }
185 }
186 #[must_use]
188 pub fn retriever(mut self, retriever: impl IntoRetriever) -> Self {
189 self.retriever = retriever.into_retriever();
190 self
191 }
192 #[cfg(feature = "retrieve-async")]
194 #[must_use]
195 pub fn async_retriever(
196 self,
197 retriever: impl IntoAsyncRetriever,
198 ) -> RegistryOptions<Arc<dyn crate::AsyncRetrieve>> {
199 RegistryOptions {
200 retriever: retriever.into_retriever(),
201 draft: self.draft,
202 }
203 }
204 pub fn build(
212 self,
213 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
214 ) -> Result<Registry, Error> {
215 Registry::try_from_resources_impl(pairs, &*self.retriever, self.draft)
216 }
217}
218
219#[cfg(feature = "retrieve-async")]
220impl RegistryOptions<Arc<dyn crate::AsyncRetrieve>> {
221 pub async fn build(
229 self,
230 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
231 ) -> Result<Registry, Error> {
232 Registry::try_from_resources_async_impl(pairs, &*self.retriever, self.draft).await
233 }
234}
235
236pub trait IntoRetriever {
237 fn into_retriever(self) -> Arc<dyn Retrieve>;
238}
239
240impl<T: Retrieve + 'static> IntoRetriever for T {
241 fn into_retriever(self) -> Arc<dyn Retrieve> {
242 Arc::new(self)
243 }
244}
245
246impl IntoRetriever for Arc<dyn Retrieve> {
247 fn into_retriever(self) -> Arc<dyn Retrieve> {
248 self
249 }
250}
251
252#[cfg(feature = "retrieve-async")]
253pub trait IntoAsyncRetriever {
254 fn into_retriever(self) -> Arc<dyn crate::AsyncRetrieve>;
255}
256
257#[cfg(feature = "retrieve-async")]
258impl<T: crate::AsyncRetrieve + 'static> IntoAsyncRetriever for T {
259 fn into_retriever(self) -> Arc<dyn crate::AsyncRetrieve> {
260 Arc::new(self)
261 }
262}
263
264#[cfg(feature = "retrieve-async")]
265impl IntoAsyncRetriever for Arc<dyn crate::AsyncRetrieve> {
266 fn into_retriever(self) -> Arc<dyn crate::AsyncRetrieve> {
267 self
268 }
269}
270
271impl Default for RegistryOptions<Arc<dyn Retrieve>> {
272 fn default() -> Self {
273 Self::new()
274 }
275}
276
277impl Registry {
278 #[must_use]
280 pub fn options() -> RegistryOptions<Arc<dyn Retrieve>> {
281 RegistryOptions::new()
282 }
283 pub fn try_new(uri: impl AsRef<str>, resource: Resource) -> Result<Self, Error> {
294 Self::try_new_impl(uri, resource, &DefaultRetriever, Draft::default())
295 }
296 pub fn try_from_resources(
306 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
307 ) -> Result<Self, Error> {
308 Self::try_from_resources_impl(pairs, &DefaultRetriever, Draft::default())
309 }
310 fn try_new_impl(
311 uri: impl AsRef<str>,
312 resource: Resource,
313 retriever: &dyn Retrieve,
314 draft: Draft,
315 ) -> Result<Self, Error> {
316 Self::try_from_resources_impl([(uri, resource)], retriever, draft)
317 }
318 fn try_from_resources_impl(
319 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
320 retriever: &dyn Retrieve,
321 draft: Draft,
322 ) -> Result<Self, Error> {
323 let mut documents = AHashMap::new();
324 let mut resources = ResourceMap::new();
325 let mut anchors = AHashMap::new();
326 let mut resolution_cache = UriCache::new();
327 let custom_metaschemas = process_resources(
328 pairs,
329 retriever,
330 &mut documents,
331 &mut resources,
332 &mut anchors,
333 &mut resolution_cache,
334 draft,
335 )?;
336
337 validate_custom_metaschemas(&custom_metaschemas, &resources)?;
339
340 Ok(Registry {
341 documents,
342 resources,
343 anchors,
344 resolution_cache: resolution_cache.into_shared(),
345 })
346 }
347 #[cfg(feature = "retrieve-async")]
357 async fn try_from_resources_async_impl(
358 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
359 retriever: &dyn crate::AsyncRetrieve,
360 draft: Draft,
361 ) -> Result<Self, Error> {
362 let mut documents = AHashMap::new();
363 let mut resources = ResourceMap::new();
364 let mut anchors = AHashMap::new();
365 let mut resolution_cache = UriCache::new();
366
367 let custom_metaschemas = process_resources_async(
368 pairs,
369 retriever,
370 &mut documents,
371 &mut resources,
372 &mut anchors,
373 &mut resolution_cache,
374 draft,
375 )
376 .await?;
377
378 validate_custom_metaschemas(&custom_metaschemas, &resources)?;
380
381 Ok(Registry {
382 documents,
383 resources,
384 anchors,
385 resolution_cache: resolution_cache.into_shared(),
386 })
387 }
388 pub fn try_with_resource(
394 self,
395 uri: impl AsRef<str>,
396 resource: Resource,
397 ) -> Result<Registry, Error> {
398 let draft = resource.draft();
399 self.try_with_resources([(uri, resource)], draft)
400 }
401 pub fn try_with_resources(
407 self,
408 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
409 draft: Draft,
410 ) -> Result<Registry, Error> {
411 self.try_with_resources_and_retriever(pairs, &DefaultRetriever, draft)
412 }
413 pub fn try_with_resources_and_retriever(
419 self,
420 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
421 retriever: &dyn Retrieve,
422 draft: Draft,
423 ) -> Result<Registry, Error> {
424 let mut documents = self.documents;
425 let mut resources = self.resources;
426 let mut anchors = self.anchors;
427 let mut resolution_cache = self.resolution_cache.into_local();
428 let custom_metaschemas = process_resources(
429 pairs,
430 retriever,
431 &mut documents,
432 &mut resources,
433 &mut anchors,
434 &mut resolution_cache,
435 draft,
436 )?;
437 validate_custom_metaschemas(&custom_metaschemas, &resources)?;
438 Ok(Registry {
439 documents,
440 resources,
441 anchors,
442 resolution_cache: resolution_cache.into_shared(),
443 })
444 }
445 #[cfg(feature = "retrieve-async")]
451 pub async fn try_with_resources_and_retriever_async(
452 self,
453 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
454 retriever: &dyn crate::AsyncRetrieve,
455 draft: Draft,
456 ) -> Result<Registry, Error> {
457 let mut documents = self.documents;
458 let mut resources = self.resources;
459 let mut anchors = self.anchors;
460 let mut resolution_cache = self.resolution_cache.into_local();
461 let custom_metaschemas = process_resources_async(
462 pairs,
463 retriever,
464 &mut documents,
465 &mut resources,
466 &mut anchors,
467 &mut resolution_cache,
468 draft,
469 )
470 .await?;
471 validate_custom_metaschemas(&custom_metaschemas, &resources)?;
472 Ok(Registry {
473 documents,
474 resources,
475 anchors,
476 resolution_cache: resolution_cache.into_shared(),
477 })
478 }
479 pub fn try_resolver(&self, base_uri: &str) -> Result<Resolver<'_>, Error> {
485 let base = uri::from_str(base_uri)?;
486 Ok(self.resolver(base))
487 }
488 #[must_use]
490 pub fn resolver(&self, base_uri: Uri<String>) -> Resolver<'_> {
491 Resolver::new(self, Arc::new(base_uri))
492 }
493 pub(crate) fn anchor<'a>(&self, uri: &'a Uri<String>, name: &'a str) -> Result<&Anchor, Error> {
494 let key = AnchorKeyRef::new(uri, name);
495 if let Some(value) = self.anchors.get(key.borrow_dyn()) {
496 return Ok(value);
497 }
498 let resource = &self.resources[uri];
499 if let Some(id) = resource.id() {
500 let uri = uri::from_str(id)?;
501 let key = AnchorKeyRef::new(&uri, name);
502 if let Some(value) = self.anchors.get(key.borrow_dyn()) {
503 return Ok(value);
504 }
505 }
506 if name.contains('/') {
507 Err(Error::invalid_anchor(name.to_string()))
508 } else {
509 Err(Error::no_such_anchor(name.to_string()))
510 }
511 }
512 pub fn resolve_against(&self, base: &Uri<&str>, uri: &str) -> Result<Arc<Uri<String>>, Error> {
518 self.resolution_cache.resolve_against(base, uri)
519 }
520 #[must_use]
526 pub fn find_vocabularies(&self, draft: Draft, contents: &Value) -> VocabularySet {
527 match draft.detect(contents) {
528 Draft::Unknown => {
529 if let Some(specification) = contents
531 .as_object()
532 .and_then(|obj| obj.get("$schema"))
533 .and_then(|s| s.as_str())
534 {
535 if let Ok(mut uri) = uri::from_str(specification) {
536 uri.set_fragment(None);
539 if let Some(resource) = self.resources.get(&uri) {
540 if let Ok(Some(vocabularies)) = vocabularies::find(resource.contents())
542 {
543 return vocabularies;
544 }
545 }
546 }
549 }
550 Draft::Unknown.default_vocabularies()
552 }
553 draft => draft.default_vocabularies(),
554 }
555 }
556
557 pub(crate) fn build_from_meta_schemas(schemas: &[(&'static str, &'static Value)]) -> Self {
559 let schemas_count = schemas.len();
560 let pairs = schemas
561 .iter()
562 .map(|(uri, schema)| (uri, ResourceRef::from_contents(schema)));
563
564 let mut documents = DocumentStore::with_capacity(schemas_count);
565 let mut resources = ResourceMap::with_capacity(schemas_count);
566
567 let mut anchors = AHashMap::with_capacity(8);
571 let mut resolution_cache = UriCache::with_capacity(35);
572
573 process_meta_schemas(
574 pairs,
575 &mut documents,
576 &mut resources,
577 &mut anchors,
578 &mut resolution_cache,
579 )
580 .expect("Failed to process meta schemas");
581
582 Self {
583 documents,
584 resources,
585 anchors,
586 resolution_cache: resolution_cache.into_shared(),
587 }
588 }
589}
590
591fn process_meta_schemas(
592 pairs: impl IntoIterator<Item = (impl AsRef<str>, ResourceRef<'static>)>,
593 documents: &mut DocumentStore,
594 resources: &mut ResourceMap,
595 anchors: &mut AHashMap<AnchorKey, Anchor>,
596 resolution_cache: &mut UriCache,
597) -> Result<(), Error> {
598 let mut queue = VecDeque::with_capacity(32);
599
600 for (uri, resource) in pairs {
601 let uri = uri::from_str(uri.as_ref().trim_end_matches('#'))?;
602 let key = Arc::new(uri);
603 let contents: &'static Value = resource.contents();
604 let wrapped_value = Arc::pin(ValueWrapper::StaticRef(contents));
605 let resource = InnerResourcePtr::new((*wrapped_value).as_ref(), resource.draft());
606 documents.insert(Arc::clone(&key), wrapped_value);
607 resources.insert(Arc::clone(&key), resource.clone());
608 queue.push_back((key, resource));
609 }
610
611 while let Some((mut base, resource)) = queue.pop_front() {
613 if let Some(id) = resource.id() {
614 base = resolution_cache.resolve_against(&base.borrow(), id)?;
615 resources.insert(base.clone(), resource.clone());
616 }
617
618 for anchor in resource.anchors() {
620 anchors.insert(AnchorKey::new(base.clone(), anchor.name()), anchor);
621 }
622
623 for contents in resource.draft().subresources_of(resource.contents()) {
625 let subresource = InnerResourcePtr::new(contents, resource.draft());
626 queue.push_back((base.clone(), subresource));
627 }
628 }
629 Ok(())
630}
631
632#[derive(Hash, Eq, PartialEq)]
633struct ReferenceKey {
634 base_ptr: NonZeroUsize,
635 reference: String,
636}
637
638impl ReferenceKey {
639 fn new(base: &Arc<Uri<String>>, reference: &str) -> Self {
640 Self {
641 base_ptr: NonZeroUsize::new(Arc::as_ptr(base) as usize)
642 .expect("Arc pointer should never be null"),
643 reference: reference.to_owned(),
644 }
645 }
646}
647
648type ReferenceTracker = AHashSet<ReferenceKey>;
649
650#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
651enum ReferenceKind {
652 Ref,
653 Schema,
654}
655
656type QueueEntry = (Arc<Uri<String>>, InnerResourcePtr, Option<Arc<Uri<String>>>);
661
662struct ProcessingState {
663 queue: VecDeque<QueueEntry>,
664 seen: ReferenceTracker,
665 external: AHashSet<(String, Uri<String>, ReferenceKind)>,
666 scratch: String,
667 refers_metaschemas: bool,
668 custom_metaschemas: Vec<Arc<Uri<String>>>,
669 visited_schemas: AHashSet<usize>,
672}
673
674impl ProcessingState {
675 fn new() -> Self {
676 Self {
677 queue: VecDeque::with_capacity(32),
678 seen: ReferenceTracker::new(),
679 external: AHashSet::new(),
680 scratch: String::new(),
681 refers_metaschemas: false,
682 custom_metaschemas: Vec::new(),
683 visited_schemas: AHashSet::new(),
684 }
685 }
686}
687
688fn process_input_resources(
689 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
690 documents: &mut DocumentStore,
691 resources: &mut ResourceMap,
692 state: &mut ProcessingState,
693) -> Result<(), Error> {
694 for (uri, resource) in pairs {
695 let uri = uri::from_str(uri.as_ref().trim_end_matches('#'))?;
696 let key = Arc::new(uri);
697 match documents.entry(Arc::clone(&key)) {
698 Entry::Occupied(_) => {}
699 Entry::Vacant(entry) => {
700 let (draft, contents) = resource.into_inner();
701 let wrapped_value = Arc::pin(ValueWrapper::Owned(contents));
702 let resource = InnerResourcePtr::new((*wrapped_value).as_ref(), draft);
703 resources.insert(Arc::clone(&key), resource.clone());
704
705 if draft == Draft::Unknown {
707 state.custom_metaschemas.push(Arc::clone(&key));
708 }
709
710 state.queue.push_back((key, resource, None));
711 entry.insert(wrapped_value);
712 }
713 }
714 }
715 Ok(())
716}
717
718fn process_queue(
719 state: &mut ProcessingState,
720 resources: &mut ResourceMap,
721 anchors: &mut AHashMap<AnchorKey, Anchor>,
722 resolution_cache: &mut UriCache,
723) -> Result<(), Error> {
724 while let Some((mut base, resource, document_root_uri)) = state.queue.pop_front() {
725 if let Some(id) = resource.id() {
726 base = resolve_id(&base, id, resolution_cache)?;
727 resources.insert(base.clone(), resource.clone());
728 }
729
730 for anchor in resource.anchors() {
731 anchors.insert(AnchorKey::new(base.clone(), anchor.name()), anchor);
732 }
733
734 let root = document_root_uri
738 .as_ref()
739 .and_then(|uri| resources.get(uri))
740 .map_or_else(|| resource.contents(), InnerResourcePtr::contents);
741
742 let contents_ptr = std::ptr::from_ref::<Value>(resource.contents()) as usize;
744 if state.visited_schemas.insert(contents_ptr) {
745 collect_external_resources(
746 &base,
747 root,
748 resource.contents(),
749 &mut state.external,
750 &mut state.seen,
751 resolution_cache,
752 &mut state.scratch,
753 &mut state.refers_metaschemas,
754 resource.draft(),
755 &mut state.visited_schemas,
756 )?;
757 }
758
759 let subresource_root_uri = document_root_uri.or_else(|| Some(base.clone()));
761 for contents in resource.draft().subresources_of(resource.contents()) {
762 let subresource = InnerResourcePtr::new(contents, resource.draft());
763 state
764 .queue
765 .push_back((base.clone(), subresource, subresource_root_uri.clone()));
766 }
767 }
768 Ok(())
769}
770
771fn handle_fragment(
772 uri: &Uri<String>,
773 resource: &InnerResourcePtr,
774 key: &Arc<Uri<String>>,
775 default_draft: Draft,
776 queue: &mut VecDeque<QueueEntry>,
777 document_root_uri: Arc<Uri<String>>,
778) {
779 if let Some(fragment) = uri.fragment() {
780 if let Some(resolved) = pointer(resource.contents(), fragment.as_str()) {
781 let draft = default_draft.detect(resolved);
782 let contents = std::ptr::addr_of!(*resolved);
783 let resource = InnerResourcePtr::new(contents, draft);
784 queue.push_back((Arc::clone(key), resource, Some(document_root_uri)));
785 }
786 }
787}
788
789fn handle_metaschemas(
790 refers_metaschemas: bool,
791 resources: &mut ResourceMap,
792 anchors: &mut AHashMap<AnchorKey, Anchor>,
793 draft_version: Draft,
794) {
795 if refers_metaschemas {
796 let schemas = metas_for_draft(draft_version);
797 let draft_registry = Registry::build_from_meta_schemas(schemas);
798 resources.reserve(draft_registry.resources.len());
799 for (key, resource) in draft_registry.resources {
800 resources.insert(key, resource.clone());
801 }
802 anchors.reserve(draft_registry.anchors.len());
803 for (key, anchor) in draft_registry.anchors {
804 anchors.insert(key, anchor);
805 }
806 }
807}
808
809fn create_resource(
810 retrieved: Value,
811 fragmentless: Uri<String>,
812 default_draft: Draft,
813 documents: &mut DocumentStore,
814 resources: &mut ResourceMap,
815 custom_metaschemas: &mut Vec<Arc<Uri<String>>>,
816) -> (Arc<Uri<String>>, InnerResourcePtr) {
817 let draft = default_draft.detect(&retrieved);
818 let wrapped_value = Arc::pin(ValueWrapper::Owned(retrieved));
819 let resource = InnerResourcePtr::new((*wrapped_value).as_ref(), draft);
820 let key = Arc::new(fragmentless);
821 documents.insert(Arc::clone(&key), wrapped_value);
822 resources.insert(Arc::clone(&key), resource.clone());
823
824 if draft == Draft::Unknown {
826 custom_metaschemas.push(Arc::clone(&key));
827 }
828
829 (key, resource)
830}
831
832fn process_resources(
833 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
834 retriever: &dyn Retrieve,
835 documents: &mut DocumentStore,
836 resources: &mut ResourceMap,
837 anchors: &mut AHashMap<AnchorKey, Anchor>,
838 resolution_cache: &mut UriCache,
839 default_draft: Draft,
840) -> Result<Vec<Arc<Uri<String>>>, Error> {
841 let mut state = ProcessingState::new();
842 process_input_resources(pairs, documents, resources, &mut state)?;
843
844 loop {
845 if state.queue.is_empty() && state.external.is_empty() {
846 break;
847 }
848
849 process_queue(&mut state, resources, anchors, resolution_cache)?;
850
851 for (original, uri, kind) in state.external.drain() {
853 let mut fragmentless = uri.clone();
854 fragmentless.set_fragment(None);
855 if !resources.contains_key(&fragmentless) {
856 let retrieved = match retriever.retrieve(&fragmentless) {
857 Ok(retrieved) => retrieved,
858 Err(error) => {
859 handle_retrieve_error(&uri, &original, &fragmentless, error, kind)?;
860 continue;
861 }
862 };
863
864 let (key, resource) = create_resource(
865 retrieved,
866 fragmentless,
867 default_draft,
868 documents,
869 resources,
870 &mut state.custom_metaschemas,
871 );
872 handle_fragment(
873 &uri,
874 &resource,
875 &key,
876 default_draft,
877 &mut state.queue,
878 Arc::clone(&key),
879 );
880 state.queue.push_back((key, resource, None));
881 }
882 }
883 }
884
885 handle_metaschemas(state.refers_metaschemas, resources, anchors, default_draft);
886
887 Ok(state.custom_metaschemas)
888}
889
890#[cfg(feature = "retrieve-async")]
891async fn process_resources_async(
892 pairs: impl IntoIterator<Item = (impl AsRef<str>, Resource)>,
893 retriever: &dyn crate::AsyncRetrieve,
894 documents: &mut DocumentStore,
895 resources: &mut ResourceMap,
896 anchors: &mut AHashMap<AnchorKey, Anchor>,
897 resolution_cache: &mut UriCache,
898 default_draft: Draft,
899) -> Result<Vec<Arc<Uri<String>>>, Error> {
900 type ExternalRefsByBase = AHashMap<Uri<String>, Vec<(String, Uri<String>, ReferenceKind)>>;
901
902 let mut state = ProcessingState::new();
903 process_input_resources(pairs, documents, resources, &mut state)?;
904
905 loop {
906 if state.queue.is_empty() && state.external.is_empty() {
907 break;
908 }
909
910 process_queue(&mut state, resources, anchors, resolution_cache)?;
911
912 if !state.external.is_empty() {
913 let mut grouped = ExternalRefsByBase::new();
917 for (original, uri, kind) in state.external.drain() {
918 let mut fragmentless = uri.clone();
919 fragmentless.set_fragment(None);
920 if !resources.contains_key(&fragmentless) {
921 grouped
922 .entry(fragmentless)
923 .or_default()
924 .push((original, uri, kind));
925 }
926 }
927
928 let entries: Vec<_> = grouped.into_iter().collect();
930 let results = {
931 let futures = entries
932 .iter()
933 .map(|(fragmentless, _)| retriever.retrieve(fragmentless));
934 futures::future::join_all(futures).await
935 };
936
937 for ((fragmentless, refs), result) in entries.into_iter().zip(results) {
938 let retrieved = match result {
939 Ok(retrieved) => retrieved,
940 Err(error) => {
941 if let Some((original, uri, kind)) = refs.into_iter().next() {
943 handle_retrieve_error(&uri, &original, &fragmentless, error, kind)?;
944 }
945 continue;
946 }
947 };
948
949 let (key, resource) = create_resource(
950 retrieved,
951 fragmentless,
952 default_draft,
953 documents,
954 resources,
955 &mut state.custom_metaschemas,
956 );
957
958 for (_, uri, _) in &refs {
960 handle_fragment(
961 uri,
962 &resource,
963 &key,
964 default_draft,
965 &mut state.queue,
966 Arc::clone(&key),
967 );
968 }
969
970 state.queue.push_back((key, resource, None));
971 }
972 }
973 }
974
975 handle_metaschemas(state.refers_metaschemas, resources, anchors, default_draft);
976
977 Ok(state.custom_metaschemas)
978}
979
980fn handle_retrieve_error(
981 uri: &Uri<String>,
982 original: &str,
983 fragmentless: &Uri<String>,
984 error: Box<dyn std::error::Error + Send + Sync>,
985 kind: ReferenceKind,
986) -> Result<(), Error> {
987 match kind {
988 ReferenceKind::Schema => {
989 Ok(())
992 }
993 ReferenceKind::Ref => {
994 if uri.scheme().as_str() == "json-schema" {
996 Err(Error::unretrievable(
997 original,
998 "No base URI is available".into(),
999 ))
1000 } else {
1001 Err(Error::unretrievable(fragmentless.as_str(), error))
1002 }
1003 }
1004 }
1005}
1006
1007fn validate_custom_metaschemas(
1008 custom_metaschemas: &[Arc<Uri<String>>],
1009 resources: &ResourceMap,
1010) -> Result<(), Error> {
1011 for uri in custom_metaschemas {
1013 if let Some(resource) = resources.get(uri) {
1014 if let Some(schema_uri) = resource
1016 .contents()
1017 .as_object()
1018 .and_then(|obj| obj.get("$schema"))
1019 .and_then(|s| s.as_str())
1020 {
1021 match uri::from_str(schema_uri) {
1023 Ok(mut meta_uri) => {
1024 meta_uri.set_fragment(None);
1026 if !resources.contains_key(&meta_uri) {
1027 return Err(Error::unknown_specification(schema_uri));
1028 }
1029 }
1030 Err(_) => {
1031 return Err(Error::unknown_specification(schema_uri));
1032 }
1033 }
1034 }
1035 }
1036 }
1037 Ok(())
1038}
1039
1040fn collect_external_resources(
1041 base: &Arc<Uri<String>>,
1042 root: &Value,
1043 contents: &Value,
1044 collected: &mut AHashSet<(String, Uri<String>, ReferenceKind)>,
1045 seen: &mut ReferenceTracker,
1046 resolution_cache: &mut UriCache,
1047 scratch: &mut String,
1048 refers_metaschemas: &mut bool,
1049 draft: Draft,
1050 visited: &mut AHashSet<usize>,
1051) -> Result<(), Error> {
1052 if base.scheme().as_str() == "urn" {
1054 return Ok(());
1055 }
1056
1057 macro_rules! on_reference {
1058 ($reference:expr, $key:literal) => {
1059 if $reference.starts_with("https://json-schema.org/draft/")
1061 || $reference.starts_with("http://json-schema.org/draft-")
1062 || base.as_str().starts_with("https://json-schema.org/draft/")
1063 {
1064 if $key == "$ref" {
1065 *refers_metaschemas = true;
1066 }
1067 } else if $reference != "#" {
1068 if mark_reference(seen, base, $reference) {
1069 if $reference.starts_with('#') {
1071 if let Some((referenced, resolved_base)) = pointer_with_base(
1075 root,
1076 $reference.trim_start_matches('#'),
1077 base,
1078 resolution_cache,
1079 draft,
1080 )? {
1081 collect_external_resources_recursive(
1083 &resolved_base,
1084 root,
1085 referenced,
1086 collected,
1087 seen,
1088 resolution_cache,
1089 scratch,
1090 refers_metaschemas,
1091 draft,
1092 visited,
1093 )?;
1094 }
1095 } else {
1096 let resolved = if base.has_fragment() {
1097 let mut base_without_fragment = base.as_ref().clone();
1098 base_without_fragment.set_fragment(None);
1099
1100 let (path, fragment) = match $reference.split_once('#') {
1101 Some((path, fragment)) => (path, Some(fragment)),
1102 None => ($reference, None),
1103 };
1104
1105 let mut resolved = (*resolution_cache
1106 .resolve_against(&base_without_fragment.borrow(), path)?)
1107 .clone();
1108 if let Some(fragment) = fragment {
1110 if let Some(encoded) = uri::EncodedString::new(fragment) {
1113 resolved = resolved.with_fragment(Some(encoded));
1114 } else {
1115 uri::encode_to(fragment, scratch);
1116 resolved = resolved.with_fragment(Some(
1117 uri::EncodedString::new_or_panic(scratch),
1118 ));
1119 scratch.clear();
1120 }
1121 }
1122 resolved
1123 } else {
1124 (*resolution_cache
1125 .resolve_against(&base.borrow(), $reference)?)
1126 .clone()
1127 };
1128
1129 let kind = if $key == "$schema" {
1130 ReferenceKind::Schema
1131 } else {
1132 ReferenceKind::Ref
1133 };
1134 collected.insert(($reference.to_string(), resolved, kind));
1135 }
1136 }
1137 }
1138 };
1139 }
1140
1141 if let Some(object) = contents.as_object() {
1142 if object.len() < 3 {
1143 for (key, value) in object {
1144 if key == "$ref" {
1145 if let Some(reference) = value.as_str() {
1146 on_reference!(reference, "$ref");
1147 }
1148 } else if key == "$schema" {
1149 if let Some(reference) = value.as_str() {
1150 on_reference!(reference, "$schema");
1151 }
1152 }
1153 }
1154 } else {
1155 if let Some(reference) = object.get("$ref").and_then(Value::as_str) {
1156 on_reference!(reference, "$ref");
1157 }
1158 if let Some(reference) = object.get("$schema").and_then(Value::as_str) {
1159 on_reference!(reference, "$schema");
1160 }
1161 }
1162 }
1163 Ok(())
1164}
1165
1166fn collect_external_resources_recursive(
1171 base: &Arc<Uri<String>>,
1172 root: &Value,
1173 contents: &Value,
1174 collected: &mut AHashSet<(String, Uri<String>, ReferenceKind)>,
1175 seen: &mut ReferenceTracker,
1176 resolution_cache: &mut UriCache,
1177 scratch: &mut String,
1178 refers_metaschemas: &mut bool,
1179 draft: Draft,
1180 visited: &mut AHashSet<usize>,
1181) -> Result<(), Error> {
1182 let ptr = std::ptr::from_ref::<Value>(contents) as usize;
1184 if !visited.insert(ptr) {
1185 return Ok(());
1186 }
1187
1188 let current_base = match draft.id_of(contents) {
1189 Some(id) => resolve_id(base, id, resolution_cache)?,
1190 None => Arc::clone(base),
1191 };
1192
1193 collect_external_resources(
1195 ¤t_base,
1196 root,
1197 contents,
1198 collected,
1199 seen,
1200 resolution_cache,
1201 scratch,
1202 refers_metaschemas,
1203 draft,
1204 visited,
1205 )?;
1206
1207 for subresource in draft.subresources_of(contents) {
1209 collect_external_resources_recursive(
1210 ¤t_base,
1211 root,
1212 subresource,
1213 collected,
1214 seen,
1215 resolution_cache,
1216 scratch,
1217 refers_metaschemas,
1218 draft,
1219 visited,
1220 )?;
1221 }
1222 Ok(())
1223}
1224
1225fn mark_reference(seen: &mut ReferenceTracker, base: &Arc<Uri<String>>, reference: &str) -> bool {
1226 seen.insert(ReferenceKey::new(base, reference))
1227}
1228
1229fn resolve_id(
1234 base: &Arc<Uri<String>>,
1235 id: &str,
1236 resolution_cache: &mut UriCache,
1237) -> Result<Arc<Uri<String>>, Error> {
1238 if id.starts_with('#') {
1239 return Ok(Arc::clone(base));
1240 }
1241 let mut resolved = (*resolution_cache.resolve_against(&base.borrow(), id)?).clone();
1242 if resolved.fragment().is_some_and(EStr::is_empty) {
1243 resolved.set_fragment(None);
1244 }
1245 Ok(Arc::new(resolved))
1246}
1247
1248pub fn pointer<'a>(document: &'a Value, pointer: &str) -> Option<&'a Value> {
1252 if pointer.is_empty() {
1253 return Some(document);
1254 }
1255 if !pointer.starts_with('/') {
1256 return None;
1257 }
1258 pointer.split('/').skip(1).map(unescape_segment).try_fold(
1259 document,
1260 |target, token| match target {
1261 Value::Object(map) => map.get(&*token),
1262 Value::Array(list) => parse_index(&token).and_then(|x| list.get(x)),
1263 _ => None,
1264 },
1265 )
1266}
1267
1268#[allow(clippy::type_complexity)]
1274fn pointer_with_base<'a>(
1275 document: &'a Value,
1276 pointer: &str,
1277 base: &Arc<Uri<String>>,
1278 resolution_cache: &mut UriCache,
1279 draft: Draft,
1280) -> Result<Option<(&'a Value, Arc<Uri<String>>)>, Error> {
1281 if pointer.is_empty() {
1282 return Ok(Some((document, Arc::clone(base))));
1283 }
1284 if !pointer.starts_with('/') {
1285 return Ok(None);
1286 }
1287
1288 let mut current = document;
1289 let mut current_base = Arc::clone(base);
1290
1291 for token in pointer.split('/').skip(1).map(unescape_segment) {
1292 if let Some(id) = draft.id_of(current) {
1294 current_base = resolve_id(¤t_base, id, resolution_cache)?;
1295 }
1296
1297 current = match current {
1298 Value::Object(map) => match map.get(&*token) {
1299 Some(v) => v,
1300 None => return Ok(None),
1301 },
1302 Value::Array(list) => match parse_index(&token).and_then(|x| list.get(x)) {
1303 Some(v) => v,
1304 None => return Ok(None),
1305 },
1306 _ => return Ok(None),
1307 };
1308 }
1309
1310 Ok(Some((current, current_base)))
1313}
1314
1315#[must_use]
1317pub fn parse_index(s: &str) -> Option<usize> {
1318 if s.starts_with('+') || (s.starts_with('0') && s.len() != 1) {
1319 return None;
1320 }
1321 s.parse().ok()
1322}
1323
1324#[cfg(test)]
1325mod tests {
1326 use std::error::Error as _;
1327
1328 use ahash::AHashMap;
1329 use fluent_uri::Uri;
1330 use serde_json::{json, Value};
1331 use test_case::test_case;
1332
1333 use crate::{uri::from_str, Draft, Registry, Resource, Retrieve};
1334
1335 use super::{pointer, RegistryOptions, SPECIFICATIONS};
1336
1337 #[test]
1338 fn test_empty_pointer() {
1339 let document = json!({});
1340 assert_eq!(pointer(&document, ""), Some(&document));
1341 }
1342
1343 #[test]
1344 fn test_invalid_uri_on_registry_creation() {
1345 let schema = Draft::Draft202012.create_resource(json!({}));
1346 let result = Registry::try_new(":/example.com", schema);
1347 let error = result.expect_err("Should fail");
1348
1349 assert_eq!(
1350 error.to_string(),
1351 "Invalid URI reference ':/example.com': unexpected character at index 0"
1352 );
1353 let source_error = error.source().expect("Should have a source");
1354 let inner_source = source_error.source().expect("Should have a source");
1355 assert_eq!(inner_source.to_string(), "unexpected character at index 0");
1356 }
1357
1358 #[test]
1359 fn test_lookup_unresolvable_url() {
1360 let schema = Draft::Draft202012.create_resource(json!({
1362 "type": "object",
1363 "properties": {
1364 "foo": { "type": "string" }
1365 }
1366 }));
1367 let registry =
1368 Registry::try_new("http://example.com/schema1", schema).expect("Invalid resources");
1369
1370 let resolver = registry
1372 .try_resolver("http://example.com/non_existent_schema")
1373 .expect("Invalid base URI");
1374
1375 let result = resolver.lookup("");
1376
1377 assert_eq!(
1378 result.unwrap_err().to_string(),
1379 "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"
1380 );
1381 }
1382
1383 #[test]
1384 fn test_relative_uri_without_base() {
1385 let schema = Draft::Draft202012.create_resource(json!({"$ref": "./virtualNetwork.json"}));
1386 let error = Registry::try_new("json-schema:///", schema).expect_err("Should fail");
1387 assert_eq!(error.to_string(), "Resource './virtualNetwork.json' is not present in a registry and retrieving it failed: No base URI is available");
1388 }
1389
1390 #[test]
1391 fn test_try_with_resources_requires_registered_custom_meta_schema() {
1392 let base_registry = Registry::try_new(
1393 "http://example.com/root",
1394 Resource::from_contents(json!({"type": "object"})),
1395 )
1396 .expect("Base registry should be created");
1397
1398 let custom_schema = Resource::from_contents(json!({
1399 "$id": "http://example.com/custom",
1400 "$schema": "http://example.com/meta/custom",
1401 "type": "string"
1402 }));
1403
1404 let error = base_registry
1405 .try_with_resources(
1406 [("http://example.com/custom", custom_schema)],
1407 Draft::default(),
1408 )
1409 .expect_err("Extending registry must fail when the custom $schema is not registered");
1410
1411 let error_msg = error.to_string();
1412 assert_eq!(
1413 error_msg,
1414 "Unknown meta-schema: 'http://example.com/meta/custom'. Custom meta-schemas must be registered in the registry before use"
1415 );
1416 }
1417
1418 #[test]
1419 fn test_try_with_resources_accepts_registered_custom_meta_schema_fragment() {
1420 let meta_schema = Resource::from_contents(json!({
1421 "$id": "http://example.com/meta/custom#",
1422 "$schema": "https://json-schema.org/draft/2020-12/schema",
1423 "type": "object"
1424 }));
1425
1426 let registry = Registry::try_new("http://example.com/meta/custom#", meta_schema)
1427 .expect("Meta-schema should be registered successfully");
1428
1429 let schema = Resource::from_contents(json!({
1430 "$id": "http://example.com/schemas/my-schema",
1431 "$schema": "http://example.com/meta/custom#",
1432 "type": "string"
1433 }));
1434
1435 registry
1436 .clone()
1437 .try_with_resources(
1438 [("http://example.com/schemas/my-schema", schema)],
1439 Draft::default(),
1440 )
1441 .expect("Schema should accept registered meta-schema URI with trailing '#'");
1442 }
1443
1444 #[test]
1445 fn test_chained_custom_meta_schemas() {
1446 let meta_schema_b = json!({
1448 "$id": "json-schema:///meta/level-b",
1449 "$schema": "https://json-schema.org/draft/2020-12/schema",
1450 "$vocabulary": {
1451 "https://json-schema.org/draft/2020-12/vocab/core": true,
1452 "https://json-schema.org/draft/2020-12/vocab/validation": true,
1453 },
1454 "type": "object",
1455 "properties": {
1456 "customProperty": {"type": "string"}
1457 }
1458 });
1459
1460 let meta_schema_a = json!({
1462 "$id": "json-schema:///meta/level-a",
1463 "$schema": "json-schema:///meta/level-b",
1464 "customProperty": "level-a-meta",
1465 "type": "object"
1466 });
1467
1468 let schema = json!({
1470 "$id": "json-schema:///schemas/my-schema",
1471 "$schema": "json-schema:///meta/level-a",
1472 "customProperty": "my-schema",
1473 "type": "string"
1474 });
1475
1476 Registry::try_from_resources([
1479 (
1480 "json-schema:///meta/level-b",
1481 Resource::from_contents(meta_schema_b),
1482 ),
1483 (
1484 "json-schema:///meta/level-a",
1485 Resource::from_contents(meta_schema_a),
1486 ),
1487 (
1488 "json-schema:///schemas/my-schema",
1489 Resource::from_contents(schema),
1490 ),
1491 ])
1492 .expect("Chained custom meta-schemas should be accepted when all are registered");
1493 }
1494
1495 struct TestRetriever {
1496 schemas: AHashMap<String, Value>,
1497 }
1498
1499 impl TestRetriever {
1500 fn new(schemas: AHashMap<String, Value>) -> Self {
1501 TestRetriever { schemas }
1502 }
1503 }
1504
1505 impl Retrieve for TestRetriever {
1506 fn retrieve(
1507 &self,
1508 uri: &Uri<String>,
1509 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
1510 if let Some(value) = self.schemas.get(uri.as_str()) {
1511 Ok(value.clone())
1512 } else {
1513 Err(format!("Failed to find {uri}").into())
1514 }
1515 }
1516 }
1517
1518 fn create_test_retriever(schemas: &[(&str, Value)]) -> TestRetriever {
1519 TestRetriever::new(
1520 schemas
1521 .iter()
1522 .map(|&(k, ref v)| (k.to_string(), v.clone()))
1523 .collect(),
1524 )
1525 }
1526
1527 struct TestCase {
1528 input_resources: Vec<(&'static str, Value)>,
1529 remote_resources: Vec<(&'static str, Value)>,
1530 expected_resolved_uris: Vec<&'static str>,
1531 }
1532
1533 #[test_case(
1534 TestCase {
1535 input_resources: vec![
1536 ("http://example.com/schema1", json!({"$ref": "http://example.com/schema2"})),
1537 ],
1538 remote_resources: vec![
1539 ("http://example.com/schema2", json!({"type": "object"})),
1540 ],
1541 expected_resolved_uris: vec!["http://example.com/schema1", "http://example.com/schema2"],
1542 }
1543 ;"External ref at top")]
1544 #[test_case(
1545 TestCase {
1546 input_resources: vec![
1547 ("http://example.com/schema1", json!({
1548 "$defs": {
1549 "subschema": {"type": "string"}
1550 },
1551 "$ref": "#/$defs/subschema"
1552 })),
1553 ],
1554 remote_resources: vec![],
1555 expected_resolved_uris: vec!["http://example.com/schema1"],
1556 }
1557 ;"Internal ref at top")]
1558 #[test_case(
1559 TestCase {
1560 input_resources: vec![
1561 ("http://example.com/schema1", json!({"$ref": "http://example.com/schema2"})),
1562 ("http://example.com/schema2", json!({"type": "object"})),
1563 ],
1564 remote_resources: vec![],
1565 expected_resolved_uris: vec!["http://example.com/schema1", "http://example.com/schema2"],
1566 }
1567 ;"Ref to later resource")]
1568 #[test_case(
1569 TestCase {
1570 input_resources: vec![
1571 ("http://example.com/schema1", json!({
1572 "type": "object",
1573 "properties": {
1574 "prop1": {"$ref": "http://example.com/schema2"}
1575 }
1576 })),
1577 ],
1578 remote_resources: vec![
1579 ("http://example.com/schema2", json!({"type": "string"})),
1580 ],
1581 expected_resolved_uris: vec!["http://example.com/schema1", "http://example.com/schema2"],
1582 }
1583 ;"External ref in subresource")]
1584 #[test_case(
1585 TestCase {
1586 input_resources: vec![
1587 ("http://example.com/schema1", json!({
1588 "type": "object",
1589 "properties": {
1590 "prop1": {"$ref": "#/$defs/subschema"}
1591 },
1592 "$defs": {
1593 "subschema": {"type": "string"}
1594 }
1595 })),
1596 ],
1597 remote_resources: vec![],
1598 expected_resolved_uris: vec!["http://example.com/schema1"],
1599 }
1600 ;"Internal ref in subresource")]
1601 #[test_case(
1602 TestCase {
1603 input_resources: vec![
1604 ("file:///schemas/main.json", json!({"$ref": "file:///schemas/external.json"})),
1605 ],
1606 remote_resources: vec![
1607 ("file:///schemas/external.json", json!({"type": "object"})),
1608 ],
1609 expected_resolved_uris: vec!["file:///schemas/main.json", "file:///schemas/external.json"],
1610 }
1611 ;"File scheme: external ref at top")]
1612 #[test_case(
1613 TestCase {
1614 input_resources: vec![
1615 ("file:///schemas/main.json", json!({"$ref": "subfolder/schema.json"})),
1616 ],
1617 remote_resources: vec![
1618 ("file:///schemas/subfolder/schema.json", json!({"type": "string"})),
1619 ],
1620 expected_resolved_uris: vec!["file:///schemas/main.json", "file:///schemas/subfolder/schema.json"],
1621 }
1622 ;"File scheme: relative path ref")]
1623 #[test_case(
1624 TestCase {
1625 input_resources: vec![
1626 ("file:///schemas/main.json", json!({
1627 "type": "object",
1628 "properties": {
1629 "local": {"$ref": "local.json"},
1630 "remote": {"$ref": "http://example.com/schema"}
1631 }
1632 })),
1633 ],
1634 remote_resources: vec![
1635 ("file:///schemas/local.json", json!({"type": "string"})),
1636 ("http://example.com/schema", json!({"type": "number"})),
1637 ],
1638 expected_resolved_uris: vec![
1639 "file:///schemas/main.json",
1640 "file:///schemas/local.json",
1641 "http://example.com/schema"
1642 ],
1643 }
1644 ;"File scheme: mixing with http scheme")]
1645 #[test_case(
1646 TestCase {
1647 input_resources: vec![
1648 ("file:///C:/schemas/main.json", json!({"$ref": "/D:/other_schemas/schema.json"})),
1649 ],
1650 remote_resources: vec![
1651 ("file:///D:/other_schemas/schema.json", json!({"type": "boolean"})),
1652 ],
1653 expected_resolved_uris: vec![
1654 "file:///C:/schemas/main.json",
1655 "file:///D:/other_schemas/schema.json"
1656 ],
1657 }
1658 ;"File scheme: absolute path in Windows style")]
1659 #[test_case(
1660 TestCase {
1661 input_resources: vec![
1662 ("http://example.com/schema1", json!({"$ref": "http://example.com/schema2"})),
1663 ],
1664 remote_resources: vec![
1665 ("http://example.com/schema2", json!({"$ref": "http://example.com/schema3"})),
1666 ("http://example.com/schema3", json!({"$ref": "http://example.com/schema4"})),
1667 ("http://example.com/schema4", json!({"$ref": "http://example.com/schema5"})),
1668 ("http://example.com/schema5", json!({"type": "object"})),
1669 ],
1670 expected_resolved_uris: vec![
1671 "http://example.com/schema1",
1672 "http://example.com/schema2",
1673 "http://example.com/schema3",
1674 "http://example.com/schema4",
1675 "http://example.com/schema5",
1676 ],
1677 }
1678 ;"Four levels of external references")]
1679 #[test_case(
1680 TestCase {
1681 input_resources: vec![
1682 ("http://example.com/schema1", json!({"$ref": "http://example.com/schema2"})),
1683 ],
1684 remote_resources: vec![
1685 ("http://example.com/schema2", json!({"$ref": "http://example.com/schema3"})),
1686 ("http://example.com/schema3", json!({"$ref": "http://example.com/schema4"})),
1687 ("http://example.com/schema4", json!({"$ref": "http://example.com/schema5"})),
1688 ("http://example.com/schema5", json!({"$ref": "http://example.com/schema6"})),
1689 ("http://example.com/schema6", json!({"$ref": "http://example.com/schema1"})),
1690 ],
1691 expected_resolved_uris: vec![
1692 "http://example.com/schema1",
1693 "http://example.com/schema2",
1694 "http://example.com/schema3",
1695 "http://example.com/schema4",
1696 "http://example.com/schema5",
1697 "http://example.com/schema6",
1698 ],
1699 }
1700 ;"Five levels of external references with circular reference")]
1701 fn test_references_processing(test_case: TestCase) {
1702 let retriever = create_test_retriever(&test_case.remote_resources);
1703
1704 let input_pairs = test_case
1705 .input_resources
1706 .clone()
1707 .into_iter()
1708 .map(|(uri, value)| (uri, Resource::from_contents(value)));
1709
1710 let registry = Registry::options()
1711 .retriever(retriever)
1712 .build(input_pairs)
1713 .expect("Invalid resources");
1714 for uri in test_case.expected_resolved_uris {
1716 let resolver = registry.try_resolver("").expect("Invalid base URI");
1717 assert!(resolver.lookup(uri).is_ok());
1718 }
1719 }
1720
1721 #[test]
1722 fn test_default_retriever_with_remote_refs() {
1723 let result = Registry::try_from_resources([(
1724 "http://example.com/schema1",
1725 Resource::from_contents(json!({"$ref": "http://example.com/schema2"})),
1726 )]);
1727 let error = result.expect_err("Should fail");
1728 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");
1729 assert!(error.source().is_some());
1730 }
1731
1732 #[test]
1733 fn test_options() {
1734 let _registry = RegistryOptions::default()
1735 .build([("", Resource::from_contents(json!({})))])
1736 .expect("Invalid resources");
1737 }
1738
1739 #[test]
1740 fn test_registry_with_duplicate_input_uris() {
1741 let input_resources = vec![
1742 (
1743 "http://example.com/schema",
1744 json!({
1745 "type": "object",
1746 "properties": {
1747 "foo": { "type": "string" }
1748 }
1749 }),
1750 ),
1751 (
1752 "http://example.com/schema",
1753 json!({
1754 "type": "object",
1755 "properties": {
1756 "bar": { "type": "number" }
1757 }
1758 }),
1759 ),
1760 ];
1761
1762 let result = Registry::try_from_resources(
1763 input_resources
1764 .into_iter()
1765 .map(|(uri, value)| (uri, Draft::Draft202012.create_resource(value))),
1766 );
1767
1768 assert!(
1769 result.is_ok(),
1770 "Failed to create registry with duplicate input URIs"
1771 );
1772 let registry = result.unwrap();
1773
1774 let resource = registry
1775 .resources
1776 .get(&from_str("http://example.com/schema").expect("Invalid URI"))
1777 .unwrap();
1778 let properties = resource
1779 .contents()
1780 .get("properties")
1781 .and_then(|v| v.as_object())
1782 .unwrap();
1783
1784 assert!(
1785 !properties.contains_key("bar"),
1786 "Registry should contain the earliest added schema"
1787 );
1788 assert!(
1789 properties.contains_key("foo"),
1790 "Registry should contain the overwritten schema"
1791 );
1792 }
1793
1794 #[test]
1795 fn test_resolver_debug() {
1796 let registry = SPECIFICATIONS
1797 .clone()
1798 .try_with_resource("http://example.com", Resource::from_contents(json!({})))
1799 .expect("Invalid resource");
1800 let resolver = registry
1801 .try_resolver("http://127.0.0.1/schema")
1802 .expect("Invalid base URI");
1803 assert_eq!(
1804 format!("{resolver:?}"),
1805 "Resolver { base_uri: \"http://127.0.0.1/schema\", scopes: \"[]\" }"
1806 );
1807 }
1808
1809 #[test]
1810 fn test_try_with_resource() {
1811 let registry = SPECIFICATIONS
1812 .clone()
1813 .try_with_resource("http://example.com", Resource::from_contents(json!({})))
1814 .expect("Invalid resource");
1815 let resolver = registry.try_resolver("").expect("Invalid base URI");
1816 let resolved = resolver
1817 .lookup("http://json-schema.org/draft-06/schema#/definitions/schemaArray")
1818 .expect("Lookup failed");
1819 assert_eq!(
1820 resolved.contents(),
1821 &json!({
1822 "type": "array",
1823 "minItems": 1,
1824 "items": { "$ref": "#" }
1825 })
1826 );
1827 }
1828
1829 #[test]
1830 fn test_invalid_reference() {
1831 let resource = Draft::Draft202012.create_resource(json!({"$schema": "$##"}));
1832 let _ = Registry::try_new("http://#/", resource);
1833 }
1834}
1835
1836#[cfg(all(test, feature = "retrieve-async"))]
1837mod async_tests {
1838 use crate::{uri, DefaultRetriever, Draft, Registry, Resource, Uri};
1839 use ahash::AHashMap;
1840 use serde_json::{json, Value};
1841 use std::{
1842 error::Error,
1843 sync::atomic::{AtomicUsize, Ordering},
1844 };
1845
1846 struct TestAsyncRetriever {
1847 schemas: AHashMap<String, Value>,
1848 }
1849
1850 impl TestAsyncRetriever {
1851 fn with_schema(uri: impl Into<String>, schema: Value) -> Self {
1852 TestAsyncRetriever {
1853 schemas: { AHashMap::from_iter([(uri.into(), schema)]) },
1854 }
1855 }
1856 }
1857
1858 #[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
1859 #[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
1860 impl crate::AsyncRetrieve for TestAsyncRetriever {
1861 async fn retrieve(
1862 &self,
1863 uri: &Uri<String>,
1864 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
1865 self.schemas
1866 .get(uri.as_str())
1867 .cloned()
1868 .ok_or_else(|| "Schema not found".into())
1869 }
1870 }
1871
1872 #[tokio::test]
1873 async fn test_default_async_retriever_with_remote_refs() {
1874 let result = Registry::options()
1875 .async_retriever(DefaultRetriever)
1876 .build([(
1877 "http://example.com/schema1",
1878 Resource::from_contents(json!({"$ref": "http://example.com/schema2"})),
1879 )])
1880 .await;
1881
1882 let error = result.expect_err("Should fail");
1883 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");
1884 assert!(error.source().is_some());
1885 }
1886
1887 #[tokio::test]
1888 async fn test_async_options() {
1889 let _registry = Registry::options()
1890 .async_retriever(DefaultRetriever)
1891 .build([("", Draft::default().create_resource(json!({})))])
1892 .await
1893 .expect("Invalid resources");
1894 }
1895
1896 #[tokio::test]
1897 async fn test_async_registry_with_duplicate_input_uris() {
1898 let input_resources = vec![
1899 (
1900 "http://example.com/schema",
1901 json!({
1902 "type": "object",
1903 "properties": {
1904 "foo": { "type": "string" }
1905 }
1906 }),
1907 ),
1908 (
1909 "http://example.com/schema",
1910 json!({
1911 "type": "object",
1912 "properties": {
1913 "bar": { "type": "number" }
1914 }
1915 }),
1916 ),
1917 ];
1918
1919 let result = Registry::options()
1920 .async_retriever(DefaultRetriever)
1921 .build(
1922 input_resources
1923 .into_iter()
1924 .map(|(uri, value)| (uri, Draft::Draft202012.create_resource(value))),
1925 )
1926 .await;
1927
1928 assert!(
1929 result.is_ok(),
1930 "Failed to create registry with duplicate input URIs"
1931 );
1932 let registry = result.unwrap();
1933
1934 let resource = registry
1935 .resources
1936 .get(&uri::from_str("http://example.com/schema").expect("Invalid URI"))
1937 .unwrap();
1938 let properties = resource
1939 .contents()
1940 .get("properties")
1941 .and_then(|v| v.as_object())
1942 .unwrap();
1943
1944 assert!(
1945 !properties.contains_key("bar"),
1946 "Registry should contain the earliest added schema"
1947 );
1948 assert!(
1949 properties.contains_key("foo"),
1950 "Registry should contain the overwritten schema"
1951 );
1952 }
1953
1954 #[tokio::test]
1955 async fn test_async_try_with_resource() {
1956 let retriever = TestAsyncRetriever::with_schema(
1957 "http://example.com/schema2",
1958 json!({"type": "object"}),
1959 );
1960
1961 let registry = Registry::options()
1962 .async_retriever(retriever)
1963 .build([(
1964 "http://example.com",
1965 Resource::from_contents(json!({"$ref": "http://example.com/schema2"})),
1966 )])
1967 .await
1968 .expect("Invalid resource");
1969
1970 let resolver = registry.try_resolver("").expect("Invalid base URI");
1971 let resolved = resolver
1972 .lookup("http://example.com/schema2")
1973 .expect("Lookup failed");
1974 assert_eq!(resolved.contents(), &json!({"type": "object"}));
1975 }
1976
1977 #[tokio::test]
1978 async fn test_async_registry_with_multiple_refs() {
1979 let retriever = TestAsyncRetriever {
1980 schemas: AHashMap::from_iter([
1981 (
1982 "http://example.com/schema2".to_string(),
1983 json!({"type": "object"}),
1984 ),
1985 (
1986 "http://example.com/schema3".to_string(),
1987 json!({"type": "string"}),
1988 ),
1989 ]),
1990 };
1991
1992 let registry = Registry::options()
1993 .async_retriever(retriever)
1994 .build([(
1995 "http://example.com/schema1",
1996 Resource::from_contents(json!({
1997 "type": "object",
1998 "properties": {
1999 "obj": {"$ref": "http://example.com/schema2"},
2000 "str": {"$ref": "http://example.com/schema3"}
2001 }
2002 })),
2003 )])
2004 .await
2005 .expect("Invalid resource");
2006
2007 let resolver = registry.try_resolver("").expect("Invalid base URI");
2008
2009 let resolved2 = resolver
2011 .lookup("http://example.com/schema2")
2012 .expect("Lookup failed");
2013 assert_eq!(resolved2.contents(), &json!({"type": "object"}));
2014
2015 let resolved3 = resolver
2016 .lookup("http://example.com/schema3")
2017 .expect("Lookup failed");
2018 assert_eq!(resolved3.contents(), &json!({"type": "string"}));
2019 }
2020
2021 #[tokio::test]
2022 async fn test_async_registry_with_nested_refs() {
2023 let retriever = TestAsyncRetriever {
2024 schemas: AHashMap::from_iter([
2025 (
2026 "http://example.com/address".to_string(),
2027 json!({
2028 "type": "object",
2029 "properties": {
2030 "street": {"type": "string"},
2031 "city": {"$ref": "http://example.com/city"}
2032 }
2033 }),
2034 ),
2035 (
2036 "http://example.com/city".to_string(),
2037 json!({
2038 "type": "string",
2039 "minLength": 1
2040 }),
2041 ),
2042 ]),
2043 };
2044
2045 let registry = Registry::options()
2046 .async_retriever(retriever)
2047 .build([(
2048 "http://example.com/person",
2049 Resource::from_contents(json!({
2050 "type": "object",
2051 "properties": {
2052 "name": {"type": "string"},
2053 "address": {"$ref": "http://example.com/address"}
2054 }
2055 })),
2056 )])
2057 .await
2058 .expect("Invalid resource");
2059
2060 let resolver = registry.try_resolver("").expect("Invalid base URI");
2061
2062 let resolved = resolver
2064 .lookup("http://example.com/city")
2065 .expect("Lookup failed");
2066 assert_eq!(
2067 resolved.contents(),
2068 &json!({"type": "string", "minLength": 1})
2069 );
2070 }
2071
2072 #[tokio::test]
2074 async fn test_async_registry_with_duplicate_fragment_refs() {
2075 static FETCH_COUNT: AtomicUsize = AtomicUsize::new(0);
2076
2077 struct CountingRetriever {
2078 inner: TestAsyncRetriever,
2079 }
2080
2081 #[cfg_attr(target_family = "wasm", async_trait::async_trait(?Send))]
2082 #[cfg_attr(not(target_family = "wasm"), async_trait::async_trait)]
2083 impl crate::AsyncRetrieve for CountingRetriever {
2084 async fn retrieve(
2085 &self,
2086 uri: &Uri<String>,
2087 ) -> Result<Value, Box<dyn std::error::Error + Send + Sync>> {
2088 FETCH_COUNT.fetch_add(1, Ordering::SeqCst);
2089 self.inner.retrieve(uri).await
2090 }
2091 }
2092
2093 FETCH_COUNT.store(0, Ordering::SeqCst);
2094
2095 let retriever = CountingRetriever {
2096 inner: TestAsyncRetriever::with_schema(
2097 "http://example.com/external",
2098 json!({
2099 "$defs": {
2100 "foo": {
2101 "type": "object",
2102 "properties": {
2103 "nested": { "type": "string" }
2104 }
2105 },
2106 "bar": {
2107 "type": "object",
2108 "properties": {
2109 "value": { "type": "integer" }
2110 }
2111 }
2112 }
2113 }),
2114 ),
2115 };
2116
2117 let registry = Registry::options()
2119 .async_retriever(retriever)
2120 .build([(
2121 "http://example.com/main",
2122 Resource::from_contents(json!({
2123 "type": "object",
2124 "properties": {
2125 "name": { "$ref": "http://example.com/external#/$defs/foo" },
2126 "age": { "$ref": "http://example.com/external#/$defs/bar" }
2127 }
2128 })),
2129 )])
2130 .await
2131 .expect("Invalid resource");
2132
2133 let fetches = FETCH_COUNT.load(Ordering::SeqCst);
2135 assert_eq!(
2136 fetches, 1,
2137 "External schema should be fetched only once, but was fetched {fetches} times"
2138 );
2139
2140 let resolver = registry
2141 .try_resolver("http://example.com/main")
2142 .expect("Invalid base URI");
2143
2144 let foo = resolver
2146 .lookup("http://example.com/external#/$defs/foo")
2147 .expect("Lookup failed");
2148 assert_eq!(
2149 foo.contents(),
2150 &json!({
2151 "type": "object",
2152 "properties": {
2153 "nested": { "type": "string" }
2154 }
2155 })
2156 );
2157
2158 let bar = resolver
2159 .lookup("http://example.com/external#/$defs/bar")
2160 .expect("Lookup failed");
2161 assert_eq!(
2162 bar.contents(),
2163 &json!({
2164 "type": "object",
2165 "properties": {
2166 "value": { "type": "integer" }
2167 }
2168 })
2169 );
2170 }
2171}