use serde::de::DeserializeOwned;
use serde_json::Value;
use std::any::{Any, TypeId};
use std::collections::BTreeMap;
use std::fs;
use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use thiserror::Error;
use url::Url;
pub type FetchFuture<'a> = Pin<Box<dyn Future<Output = Result<Value, LoaderError>> + 'a>>;
pub trait ResourceFetcher {
fn fetch(&mut self, uri: &Url) -> Result<Value, LoaderError>;
}
pub trait AsyncResourceFetcher {
fn fetch<'a>(&'a mut self, uri: &'a Url) -> FetchFuture<'a>;
}
#[derive(Clone, Debug, Default)]
pub struct JsonFileFetcher;
impl ResourceFetcher for JsonFileFetcher {
fn fetch(&mut self, uri: &Url) -> Result<Value, LoaderError> {
if uri.scheme() != "file" {
return Err(LoaderError::UnsupportedFetcherUri(uri.as_str().to_string()));
}
let path = uri
.to_file_path()
.map_err(|()| LoaderError::InvalidFileUri(uri.as_str().to_string()))?;
let bytes = fs::read(&path).map_err(|source| LoaderError::ReadFile { path, source })?;
serde_json::from_slice(&bytes).map_err(|source| LoaderError::Parse {
uri: uri.as_str().to_string(),
source,
})
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum LoaderError {
#[error("no fetcher registered for `{uri}`")]
NoFetcherRegistered { uri: String },
#[error("fetcher does not support `{0}`")]
UnsupportedFetcherUri(String),
#[error("invalid file URI `{0}`")]
InvalidFileUri(String),
#[error(
"reference `{0}` has no base resource — loader cannot resolve internal-only `#/...` references"
)]
MissingBaseUri(String),
#[error("invalid URI `{uri}`")]
InvalidUri {
uri: String,
#[source]
source: url::ParseError,
},
#[error("failed to read `{path}`")]
ReadFile {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("failed to parse `{uri}`")]
Parse {
uri: String,
#[source]
source: serde_json::Error,
},
#[error("failed to fetch `{uri}`")]
Fetch {
uri: String,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("reference `{reference}` not found in `{uri}`")]
PointerNotFound { uri: String, reference: String },
#[error("invalid URI fragment in `{0}`")]
InvalidFragment(String),
}
pub struct Loader {
fetchers: BTreeMap<String, Box<dyn ResourceFetcher>>,
async_fetchers: BTreeMap<String, Box<dyn AsyncResourceFetcher>>,
cache: BTreeMap<Url, Value>,
typed_cache: BTreeMap<(String, TypeId), Box<dyn Any>>,
}
impl Loader {
pub fn new() -> Self {
Self {
fetchers: BTreeMap::new(),
async_fetchers: BTreeMap::new(),
cache: BTreeMap::new(),
typed_cache: BTreeMap::new(),
}
}
pub fn register_fetcher(
&mut self,
prefix: impl Into<String>,
fetcher: impl ResourceFetcher + 'static,
) -> Option<Box<dyn ResourceFetcher>> {
self.fetchers.insert(prefix.into(), Box::new(fetcher))
}
pub fn register_async_fetcher(
&mut self,
prefix: impl Into<String>,
fetcher: impl AsyncResourceFetcher + 'static,
) -> Option<Box<dyn AsyncResourceFetcher>> {
self.async_fetchers.insert(prefix.into(), Box::new(fetcher))
}
pub fn preload_resource(
&mut self,
uri: impl AsRef<str>,
document: Value,
) -> Result<Option<Value>, LoaderError> {
let (key, _) = parse_reference(uri.as_ref())?;
let mut document = document;
rewrite_refs_against(&mut document, &key);
let previous = self.cache.insert(key, document);
if previous.is_some() {
self.typed_cache.clear();
}
Ok(previous)
}
pub fn load_resource(&mut self, uri: &str) -> Result<&Value, LoaderError> {
let (key, _) = parse_reference(uri)?;
self.load_resource_by_key(key)
}
fn load_resource_by_key(&mut self, key: Url) -> Result<&Value, LoaderError> {
if !self.cache.contains_key(&key) {
let fetcher_key = best_fetcher_key(&self.fetchers, key.as_str()).ok_or_else(|| {
LoaderError::NoFetcherRegistered {
uri: key.as_str().to_string(),
}
})?;
let mut parsed = self
.fetchers
.get_mut(&fetcher_key)
.expect("fetcher key came from the registry")
.fetch(&key)?;
rewrite_refs_against(&mut parsed, &key);
self.cache.insert(key.clone(), parsed);
}
Ok(self
.cache
.get(&key)
.expect("resource was inserted into the cache"))
}
pub async fn load_resource_async(&mut self, uri: &str) -> Result<&Value, LoaderError> {
let (key, _) = parse_reference(uri)?;
self.load_resource_by_key_async(key).await
}
async fn load_resource_by_key_async(&mut self, key: Url) -> Result<&Value, LoaderError> {
if !self.cache.contains_key(&key) {
let fetcher_key =
best_fetcher_key(&self.async_fetchers, key.as_str()).ok_or_else(|| {
LoaderError::NoFetcherRegistered {
uri: key.as_str().to_string(),
}
})?;
let mut parsed = self
.async_fetchers
.get_mut(&fetcher_key)
.expect("async fetcher key came from the registry")
.fetch(&key)
.await?;
rewrite_refs_against(&mut parsed, &key);
self.cache.insert(key.clone(), parsed);
}
Ok(self
.cache
.get(&key)
.expect("resource was inserted into the cache"))
}
pub fn resolve_reference(&mut self, reference: &str) -> Result<&Value, LoaderError> {
let (key, fragment) = parse_reference(reference)?;
if key.as_str() == "relative:" {
return Err(LoaderError::MissingBaseUri(reference.to_string()));
}
let pointer = decode_fragment(&fragment)
.map_err(|()| LoaderError::InvalidFragment(reference.to_string()))?;
let document = self.load_resource_by_key(key.clone())?;
if pointer.is_empty() {
return Ok(document);
}
match document.pointer(&pointer) {
Some(value) => Ok(value),
None => Err(LoaderError::PointerNotFound {
uri: key.as_str().to_string(),
reference: reference.to_string(),
}),
}
}
pub async fn resolve_reference_async(
&mut self,
reference: &str,
) -> Result<&Value, LoaderError> {
let (key, fragment) = parse_reference(reference)?;
if key.as_str() == "relative:" {
return Err(LoaderError::MissingBaseUri(reference.to_string()));
}
let pointer = decode_fragment(&fragment)
.map_err(|()| LoaderError::InvalidFragment(reference.to_string()))?;
let document = self.load_resource_by_key_async(key.clone()).await?;
if pointer.is_empty() {
return Ok(document);
}
match document.pointer(&pointer) {
Some(value) => Ok(value),
None => Err(LoaderError::PointerNotFound {
uri: key.as_str().to_string(),
reference: reference.to_string(),
}),
}
}
fn typed_cache_get<T: 'static>(&self, cache_key: &(String, TypeId)) -> Option<Arc<T>> {
self.typed_cache
.get(cache_key)?
.downcast_ref::<Arc<T>>()
.cloned()
}
pub fn resolve_reference_as_arc<T>(&mut self, reference: &str) -> Result<Arc<T>, LoaderError>
where
T: 'static + DeserializeOwned,
{
let cache_key = (reference.to_string(), TypeId::of::<T>());
if let Some(cached) = self.typed_cache_get::<T>(&cache_key) {
return Ok(cached);
}
let (key, _) = parse_reference(reference)?;
let uri = key.to_string();
let value = self.resolve_reference(reference)?;
let parsed: T = serde_json::from_value(value.clone())
.map_err(|source| LoaderError::Parse { uri, source })?;
let arc = Arc::new(parsed);
self.typed_cache
.insert(cache_key, Box::new(Arc::clone(&arc)));
Ok(arc)
}
pub fn resolve_reference_as<T>(&mut self, reference: &str) -> Result<T, LoaderError>
where
T: 'static + Clone + DeserializeOwned,
{
self.resolve_reference_as_arc::<T>(reference)
.map(|arc| (*arc).clone())
}
pub async fn resolve_reference_as_arc_async<T>(
&mut self,
reference: &str,
) -> Result<Arc<T>, LoaderError>
where
T: 'static + DeserializeOwned,
{
let cache_key = (reference.to_string(), TypeId::of::<T>());
if let Some(cached) = self.typed_cache_get::<T>(&cache_key) {
return Ok(cached);
}
let (key, _) = parse_reference(reference)?;
let uri = key.to_string();
let value = self.resolve_reference_async(reference).await?;
let parsed: T = serde_json::from_value(value.clone())
.map_err(|source| LoaderError::Parse { uri, source })?;
let arc = Arc::new(parsed);
self.typed_cache
.insert(cache_key, Box::new(Arc::clone(&arc)));
Ok(arc)
}
pub async fn resolve_reference_as_async<T>(&mut self, reference: &str) -> Result<T, LoaderError>
where
T: 'static + Clone + DeserializeOwned,
{
self.resolve_reference_as_arc_async::<T>(reference)
.await
.map(|arc| (*arc).clone())
}
}
impl Default for Loader {
fn default() -> Self {
Self::new()
}
}
fn best_fetcher_key<T: ?Sized>(fetchers: &BTreeMap<String, Box<T>>, uri: &str) -> Option<String> {
fetchers
.keys()
.filter(|prefix| uri.starts_with(prefix.as_str()))
.max_by_key(|prefix| prefix.len())
.cloned()
}
fn rewrite_refs_against(value: &mut Value, base: &Url) {
match value {
Value::Object(map) => {
if let Some(Value::String(s)) = map.get_mut("$ref")
&& let Ok(joined) = base.join(s)
{
*s = joined.to_string();
}
for v in map.values_mut() {
rewrite_refs_against(v, base);
}
}
Value::Array(items) => {
for v in items.iter_mut() {
rewrite_refs_against(v, base);
}
}
_ => {}
}
}
fn parse_reference(reference: &str) -> Result<(Url, String), LoaderError> {
match Url::parse(reference) {
Ok(url) => Ok(split_url_fragment(url)),
Err(url::ParseError::RelativeUrlWithoutBase) => {
let relative = format!("relative:{reference}");
Url::parse(&relative)
.map_err(|source| LoaderError::InvalidUri {
uri: reference.to_string(),
source,
})
.map(split_url_fragment)
}
Err(source) => Err(LoaderError::InvalidUri {
uri: reference.to_string(),
source,
}),
}
}
fn split_url_fragment(mut url: Url) -> (Url, String) {
let fragment = url.fragment().unwrap_or_default().to_string();
url.set_fragment(None);
(url, fragment)
}
fn decode_fragment(fragment: &str) -> Result<String, ()> {
if fragment.is_empty() {
return Ok(String::new());
}
let bytes = fragment.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' {
if i + 2 >= bytes.len() {
return Err(());
}
let hi = hex(bytes[i + 1])?;
let lo = hex(bytes[i + 2])?;
out.push((hi << 4) | lo);
i += 3;
} else {
out.push(bytes[i]);
i += 1;
}
}
let decoded = String::from_utf8(out).map_err(|_| ())?;
if !decoded.starts_with('/') {
return Err(());
}
Ok(decoded)
}
fn hex(byte: u8) -> Result<u8, ()> {
match byte {
b'0'..=b'9' => Ok(byte - b'0'),
b'a'..=b'f' => Ok(byte - b'a' + 10),
b'A'..=b'F' => Ok(byte - b'A' + 10),
_ => Err(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use std::future::Future;
use std::pin::pin;
use std::rc::Rc;
use std::task::{Context, Poll, Waker};
#[derive(Clone, Default)]
struct StaticFetcher {
count: Rc<Cell<usize>>,
}
impl ResourceFetcher for StaticFetcher {
fn fetch(&mut self, _uri: &Url) -> Result<Value, LoaderError> {
self.count.set(self.count.get() + 1);
Ok(pet_document())
}
}
#[derive(Clone, Default)]
struct AsyncStaticFetcher {
count: Rc<Cell<usize>>,
}
impl AsyncResourceFetcher for AsyncStaticFetcher {
fn fetch<'a>(&'a mut self, _uri: &'a Url) -> FetchFuture<'a> {
Box::pin(async move {
self.count.set(self.count.get() + 1);
Ok(pet_document())
})
}
}
fn pet_document() -> Value {
serde_json::json!({
"components": {
"schemas": {
"Pet": {
"type": "object"
}
}
}
})
}
fn block_on<F: Future>(future: F) -> F::Output {
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let mut future = pin!(future);
loop {
match future.as_mut().poll(&mut cx) {
Poll::Ready(value) => return value,
Poll::Pending => std::thread::yield_now(),
}
}
}
#[test]
fn default_loader_has_no_fetchers() {
let mut loader = Loader::new();
let err = loader
.load_resource("Cargo.toml")
.expect_err("file loading should not happen without a fetcher");
assert!(matches!(err, LoaderError::NoFetcherRegistered { .. }));
}
#[test]
fn file_resource_is_fetched_once_and_cached() {
let dir = std::env::temp_dir();
let file = dir.join(format!(
"roas-loader-test-{}-{}.json",
std::process::id(),
"cache"
));
fs::write(
&file,
br#"{"components":{"schemas":{"Pet":{"type":"object"}}}}"#,
)
.unwrap();
let mut loader = Loader::new();
loader.register_fetcher("file://", JsonFileFetcher);
let mut url = Url::from_file_path(&file).unwrap();
url.set_fragment(Some("/components/schemas/Pet"));
let reference = url.to_string();
assert!(loader.resolve_reference(&reference).is_ok());
fs::remove_file(&file).unwrap();
assert!(
loader.resolve_reference(&reference).is_ok(),
"second resolve should hit the cache, not re-fetch the deleted file"
);
}
#[test]
fn relative_reference_uses_preloaded_document() {
let mut loader = Loader::new();
loader
.preload_resource(
"common.json",
serde_json::json!({
"Pet": {
"type": "object"
}
}),
)
.unwrap();
let value = loader.resolve_reference("common.json#/Pet/type").unwrap();
assert_eq!(value, "object");
}
#[test]
fn registered_uri_fetcher_is_opt_in_and_cached() {
let fetch_count = Rc::new(Cell::new(0));
let mut loader = Loader::new();
loader.register_fetcher(
"https://",
StaticFetcher {
count: fetch_count.clone(),
},
);
let reference = "https://example.test/openapi.json#/components/schemas/Pet";
assert!(loader.resolve_reference(reference).is_ok());
assert!(loader.resolve_reference(reference).is_ok());
assert_eq!(fetch_count.get(), 1);
}
#[test]
fn registered_async_fetcher_is_opt_in_and_cached() {
let fetch_count = Rc::new(Cell::new(0));
let mut loader = Loader::new();
loader.register_async_fetcher(
"https://",
AsyncStaticFetcher {
count: fetch_count.clone(),
},
);
let reference = "https://example.test/openapi.json#/components/schemas/Pet";
assert!(block_on(loader.resolve_reference_async(reference)).is_ok());
assert!(block_on(loader.resolve_reference_async(reference)).is_ok());
assert_eq!(fetch_count.get(), 1);
}
#[test]
fn longest_fetcher_prefix_wins() {
let broad_count = Rc::new(Cell::new(0));
let narrow_count = Rc::new(Cell::new(0));
let mut loader = Loader::new();
loader.register_fetcher(
"https://",
StaticFetcher {
count: broad_count.clone(),
},
);
loader.register_fetcher(
"https://schemas.example.test/",
StaticFetcher {
count: narrow_count.clone(),
},
);
let reference = "https://schemas.example.test/openapi.json#/components/schemas/Pet";
assert!(loader.resolve_reference(reference).is_ok());
assert_eq!(broad_count.get(), 0);
assert_eq!(narrow_count.get(), 1);
}
#[test]
fn async_loader_uses_async_fetchers_only() {
let sync_count = Rc::new(Cell::new(0));
let async_count = Rc::new(Cell::new(0));
let mut loader = Loader::new();
loader.register_fetcher(
"https://schemas.example.test/",
StaticFetcher {
count: sync_count.clone(),
},
);
loader.register_async_fetcher(
"https://",
AsyncStaticFetcher {
count: async_count.clone(),
},
);
let reference = "https://schemas.example.test/openapi.json#/components/schemas/Pet";
assert!(block_on(loader.resolve_reference_async(reference)).is_ok());
assert_eq!(sync_count.get(), 0);
assert_eq!(async_count.get(), 1);
}
#[test]
fn async_loader_ignores_sync_fetchers_when_cache_misses() {
let sync_count = Rc::new(Cell::new(0));
let mut loader = Loader::new();
loader.register_fetcher(
"https://",
StaticFetcher {
count: sync_count.clone(),
},
);
let reference = "https://example.test/openapi.json#/components/schemas/Pet";
let err = block_on(loader.resolve_reference_async(reference))
.expect_err("async loading should require an async fetcher");
assert!(
matches!(err, LoaderError::NoFetcherRegistered { uri } if uri == "https://example.test/openapi.json")
);
assert_eq!(sync_count.get(), 0);
}
#[test]
fn relative_reference_without_preload_uses_resource_as_cache_key() {
let mut loader = Loader::new();
loader.register_fetcher("https://", StaticFetcher::default());
let err = loader
.resolve_reference("../common.json#/components/schemas/Pet")
.expect_err("relative external refs must be preloaded explicitly");
assert!(
matches!(err, LoaderError::NoFetcherRegistered { uri } if uri == "relative:../common.json")
);
}
#[test]
fn query_only_reference_uses_query_as_cache_key() {
let mut loader = Loader::new();
loader
.preload_resource(
"?v=2",
serde_json::json!({
"Pet": {
"type": "object"
}
}),
)
.unwrap();
let value = loader.resolve_reference("?v=2#/Pet/type").unwrap();
assert_eq!(value, "object");
}
#[test]
fn parse_reference_returns_resource_url_and_fragment() {
let (key, fragment) =
parse_reference("https://example.test/document.json#/foo/bar").unwrap();
assert_eq!(key.as_str(), "https://example.test/document.json");
assert_eq!(fragment, "/foo/bar");
let (key, fragment) = parse_reference("file://content.json#/foo/bar").unwrap();
assert_eq!(key.as_str(), "file://content.json/");
assert_eq!(fragment, "/foo/bar");
let (key, fragment) = parse_reference("content.json#/foo/bar").unwrap();
assert_eq!(key.scheme(), "relative");
assert_eq!(key.as_str(), "relative:content.json");
assert_eq!(fragment, "/foo/bar");
}
#[test]
fn internal_reference_without_resource_is_rejected() {
let mut loader = Loader::new();
let err = loader
.resolve_reference("#/components/schemas/Pet")
.expect_err("loader only resolves external references");
assert!(matches!(err, LoaderError::MissingBaseUri(_)));
}
#[test]
fn preload_overwrite_invalidates_typed_cache() {
#[derive(Clone, serde::Deserialize, PartialEq, Debug)]
struct Pet {
name: String,
}
let mut loader = Loader::new();
loader
.preload_resource("pets.json", serde_json::json!({ "Pet": { "name": "rex" } }))
.unwrap();
let first: Pet = loader.resolve_reference_as("pets.json#/Pet").unwrap();
assert_eq!(first.name, "rex");
loader
.preload_resource(
"pets.json",
serde_json::json!({ "Pet": { "name": "buddy" } }),
)
.unwrap();
let second: Pet = loader.resolve_reference_as("pets.json#/Pet").unwrap();
assert_eq!(
second.name, "buddy",
"typed cache must be invalidated when the underlying document is re-preloaded"
);
}
#[test]
fn json_file_fetcher_rejects_non_file_scheme() {
let url = Url::parse("https://example.test/foo.json").unwrap();
let err = JsonFileFetcher
.fetch(&url)
.expect_err("non-file must error");
assert!(matches!(err, LoaderError::UnsupportedFetcherUri(u) if u.contains("https")));
}
#[test]
fn json_file_fetcher_surfaces_missing_file_as_read_error() {
let url = Url::parse("file:///does/not/exist/roas-loader-test.json").unwrap();
let err = JsonFileFetcher
.fetch(&url)
.expect_err("missing file must error");
assert!(
matches!(err, LoaderError::ReadFile { path, .. } if path.to_string_lossy().contains("does/not/exist"))
);
}
#[test]
fn json_file_fetcher_surfaces_invalid_json_as_parse_error() {
let file = std::env::temp_dir().join(format!(
"roas-loader-test-{}-invalid.json",
std::process::id()
));
fs::write(&file, b"not valid json").unwrap();
let url = Url::from_file_path(&file).unwrap();
let err = JsonFileFetcher
.fetch(&url)
.expect_err("invalid JSON must error");
assert!(matches!(err, LoaderError::Parse { .. }));
fs::remove_file(file).unwrap();
}
#[test]
fn loader_error_fetch_carries_arbitrary_source_and_displays_uri() {
let inner = std::io::Error::other("connection refused");
let err = LoaderError::Fetch {
uri: "https://example.test/pet.json".into(),
source: Box::new(inner),
};
assert_eq!(
err.to_string(),
"failed to fetch `https://example.test/pet.json`",
);
let source = std::error::Error::source(&err).expect("Fetch must expose a source");
assert_eq!(source.to_string(), "connection refused");
}
#[test]
fn resolve_reference_propagates_pointer_not_found() {
let mut loader = Loader::new();
loader
.preload_resource("doc.json", serde_json::json!({ "Pet": { "name": "x" } }))
.unwrap();
let err = loader
.resolve_reference("doc.json#/Missing")
.expect_err("nonexistent pointer must error");
assert!(matches!(err, LoaderError::PointerNotFound { .. }));
}
#[test]
fn resolve_reference_with_invalid_fragment_errors() {
let mut loader = Loader::new();
loader
.preload_resource("doc.json", serde_json::json!({ "ok": true }))
.unwrap();
let err = loader
.resolve_reference("doc.json#%ZZ")
.expect_err("invalid percent-encoding in fragment must error");
assert!(matches!(err, LoaderError::InvalidFragment(_)));
}
#[test]
fn resolve_reference_with_non_pointer_fragment_errors() {
let mut loader = Loader::new();
loader
.preload_resource("doc.json", serde_json::json!({ "foo": true }))
.unwrap();
let err = loader
.resolve_reference("doc.json#foo")
.expect_err("fragment without leading `/` is not a JSON Pointer");
assert!(matches!(err, LoaderError::InvalidFragment(_)));
}
#[test]
fn resolve_reference_with_empty_fragment_returns_full_document() {
let mut loader = Loader::new();
loader
.preload_resource("doc.json", serde_json::json!({ "ok": true }))
.unwrap();
let value = loader.resolve_reference("doc.json").unwrap();
assert_eq!(value, &serde_json::json!({ "ok": true }));
}
#[test]
fn resolve_reference_as_uses_typed_cache_on_repeat_calls() {
#[derive(Clone, serde::Deserialize, PartialEq, Debug)]
struct Pet {
name: String,
}
let mut loader = Loader::new();
loader
.preload_resource("doc.json", serde_json::json!({ "Pet": { "name": "rex" } }))
.unwrap();
let first: Pet = loader.resolve_reference_as("doc.json#/Pet").unwrap();
let mut url = Url::parse("relative:doc.json").unwrap();
url.set_fragment(None);
loader
.cache
.insert(url, serde_json::json!({ "Pet": { "name": "different" } }));
let second: Pet = loader.resolve_reference_as("doc.json#/Pet").unwrap();
assert_eq!(first, second, "typed cache must keep the parsed value");
}
#[test]
fn resolve_reference_as_arc_shares_one_allocation() {
#[derive(Clone, serde::Deserialize)]
struct Pet {
name: String,
}
let mut loader = Loader::new();
loader
.preload_resource("doc.json", serde_json::json!({ "Pet": { "name": "rex" } }))
.unwrap();
let first: Arc<Pet> = loader.resolve_reference_as_arc("doc.json#/Pet").unwrap();
let second: Arc<Pet> = loader.resolve_reference_as_arc("doc.json#/Pet").unwrap();
assert!(
Arc::ptr_eq(&first, &second),
"repeat calls must share one Arc allocation"
);
assert_eq!(first.name, "rex");
let owned: Pet = loader.resolve_reference_as("doc.json#/Pet").unwrap();
assert_eq!(owned.name, "rex");
}
fn block_on_simple<F: Future>(future: F) -> F::Output {
use std::pin::pin;
use std::task::{Context, Poll, Waker};
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let mut future = pin!(future);
loop {
match future.as_mut().poll(&mut cx) {
Poll::Ready(value) => return value,
Poll::Pending => std::thread::yield_now(),
}
}
}
#[test]
fn load_resource_async_returns_preloaded_value_without_fetcher() {
let mut loader = Loader::new();
loader
.preload_resource("doc.json", serde_json::json!({ "ok": true }))
.unwrap();
let value = block_on_simple(loader.load_resource_async("doc.json")).unwrap();
assert_eq!(value, &serde_json::json!({ "ok": true }));
}
#[test]
fn resolve_reference_async_pointer_into_preloaded_document() {
let mut loader = Loader::new();
loader
.preload_resource("doc.json", serde_json::json!({ "Pet": { "name": "rex" } }))
.unwrap();
let value = block_on_simple(loader.resolve_reference_async("doc.json#/Pet/name")).unwrap();
assert_eq!(value, &serde_json::json!("rex"));
}
#[test]
fn resolve_reference_async_rejects_internal_only_refs() {
let mut loader = Loader::new();
let err = block_on_simple(loader.resolve_reference_async("#/components/schemas/Pet"))
.expect_err("internal refs are not loader resolvable");
assert!(matches!(err, LoaderError::MissingBaseUri(_)));
}
#[test]
fn resolve_reference_as_async_uses_shared_typed_cache() {
#[derive(Clone, serde::Deserialize, PartialEq, Debug)]
struct Pet {
name: String,
}
let mut loader = Loader::new();
loader
.preload_resource("doc.json", serde_json::json!({ "Pet": { "name": "rex" } }))
.unwrap();
let sync_first: Pet = loader.resolve_reference_as("doc.json#/Pet").unwrap();
let mut url = Url::parse("relative:doc.json").unwrap();
url.set_fragment(None);
loader
.cache
.insert(url, serde_json::json!({ "Pet": { "name": "different" } }));
let async_second: Pet =
block_on_simple(loader.resolve_reference_as_async("doc.json#/Pet")).unwrap();
assert_eq!(
sync_first, async_second,
"sync and async share the typed cache"
);
}
#[test]
fn resolve_reference_async_pointer_not_found_errors() {
let mut loader = Loader::new();
loader
.preload_resource("doc.json", serde_json::json!({ "Pet": { "name": "x" } }))
.unwrap();
let err = block_on_simple(loader.resolve_reference_async("doc.json#/Missing"))
.expect_err("nonexistent pointer must error in async path");
assert!(matches!(err, LoaderError::PointerNotFound { .. }));
}
#[test]
fn resolve_reference_async_invalid_fragment_errors() {
let mut loader = Loader::new();
loader
.preload_resource("doc.json", serde_json::json!({ "ok": true }))
.unwrap();
let err = block_on_simple(loader.resolve_reference_async("doc.json#%ZZ"))
.expect_err("invalid percent-encoding must error in async path");
assert!(matches!(err, LoaderError::InvalidFragment(_)));
}
#[test]
fn resolve_reference_as_async_fresh_parse_when_cache_cold() {
#[derive(Clone, serde::Deserialize, PartialEq, Debug)]
struct Pet {
name: String,
}
let mut loader = Loader::new();
loader
.preload_resource("doc.json", serde_json::json!({ "Pet": { "name": "rex" } }))
.unwrap();
let pet: Pet = block_on_simple(loader.resolve_reference_as_async("doc.json#/Pet")).unwrap();
assert_eq!(pet.name, "rex");
}
#[test]
fn resolve_reference_as_async_parse_error_reported() {
#[derive(Clone, serde::Deserialize, PartialEq, Debug)]
struct Pet {
name: String,
}
let mut loader = Loader::new();
loader
.preload_resource("doc.json", serde_json::json!({ "Pet": { "no_name": 42 } }))
.unwrap();
let err = block_on_simple(loader.resolve_reference_as_async::<Pet>("doc.json#/Pet"))
.expect_err("serde mismatch must produce a Parse error");
assert!(matches!(err, LoaderError::Parse { .. }), "got {err:?}");
}
#[test]
fn resolve_reference_as_sync_parse_error_reported() {
#[derive(Clone, serde::Deserialize, PartialEq, Debug)]
struct Pet {
name: String,
}
let mut loader = Loader::new();
loader
.preload_resource("doc.json", serde_json::json!({ "Pet": { "no_name": 42 } }))
.unwrap();
let err = loader
.resolve_reference_as::<Pet>("doc.json#/Pet")
.expect_err("serde mismatch must produce a Parse error");
assert!(matches!(err, LoaderError::Parse { .. }), "got {err:?}");
}
#[test]
fn loader_default_impl_matches_new() {
let loader = Loader::default();
let mut loader = loader;
let err = loader
.load_resource("https://example.test/doc.json")
.expect_err("default loader must have no fetchers");
assert!(matches!(err, LoaderError::NoFetcherRegistered { .. }));
}
#[test]
fn loader_error_display_variants() {
let e = LoaderError::NoFetcherRegistered {
uri: "https://x/y".into(),
};
assert!(e.to_string().contains("no fetcher"));
let e = LoaderError::UnsupportedFetcherUri("https://x/y".into());
assert!(e.to_string().contains("does not support"));
let e = LoaderError::InvalidFileUri("file://x".into());
assert!(e.to_string().contains("invalid file URI"));
let e = LoaderError::MissingBaseUri("#/x".into());
assert!(e.to_string().contains("no base resource"));
let e = LoaderError::PointerNotFound {
uri: "doc.json".into(),
reference: "doc.json#/Missing".into(),
};
assert!(e.to_string().contains("not found"));
let e = LoaderError::InvalidFragment("doc.json#foo".into());
assert!(e.to_string().contains("invalid URI fragment"));
}
#[test]
fn decode_fragment_invalid_hex_digit_errors() {
let mut loader = Loader::new();
loader
.preload_resource("doc.json", serde_json::json!({ "ok": true }))
.unwrap();
let err = loader
.resolve_reference("doc.json#%2G")
.expect_err("invalid hex digit must error");
assert!(
matches!(err, LoaderError::InvalidFragment(_)),
"got {err:?}"
);
}
#[test]
fn decode_fragment_truncated_percent_sequence_errors() {
let mut loader = Loader::new();
let mut key = Url::parse("https://example.test/doc.json").unwrap();
let doc = serde_json::json!({ "ok": true });
loader.preload_resource(key.as_str(), doc).unwrap();
key.set_fragment(Some("/foo%"));
let reference = key.to_string(); let result = loader.resolve_reference(&reference);
assert!(result.is_err(), "truncated/escaped percent must error");
}
#[test]
fn decode_fragment_invalid_utf8_errors() {
let mut loader = Loader::new();
let mut key = Url::parse("https://example.test/doc.json").unwrap();
loader
.preload_resource(key.as_str(), serde_json::json!({ "ok": true }))
.unwrap();
key.set_fragment(Some("/%80"));
let reference = key.to_string();
let result = loader.resolve_reference(&reference);
assert!(result.is_err(), "invalid-UTF-8 fragment must not succeed");
}
#[test]
fn parse_reference_invalid_non_relative_uri_errors() {
let mut loader = Loader::new();
let err = loader
.load_resource("http://example.test:99999/path")
.expect_err("invalid-port URL must error");
assert!(
matches!(
err,
LoaderError::InvalidUri { .. } | LoaderError::NoFetcherRegistered { .. }
),
"expected InvalidUri or NoFetcherRegistered, got {err:?}"
);
}
#[test]
fn resolve_reference_async_empty_fragment_returns_full_document() {
let mut loader = Loader::new();
loader
.preload_resource("doc.json", serde_json::json!({ "ok": true }))
.unwrap();
let value = block_on_simple(loader.resolve_reference_async("doc.json")).unwrap();
assert_eq!(value, &serde_json::json!({ "ok": true }));
}
#[test]
fn decode_fragment_lowercase_hex_digits_are_accepted() {
let mut loader = Loader::new();
loader
.preload_resource(
"https://example.test/doc.json",
serde_json::json!({ "foo": { "bar": 42 } }),
)
.unwrap();
let value = loader
.resolve_reference("https://example.test/doc.json#/foo%2fbar")
.unwrap();
assert_eq!(value, &serde_json::json!(42));
}
#[test]
fn parse_reference_relative_with_control_char_produces_invalid_uri_error() {
let mut loader = Loader::new();
let reference = "doc\x00.json";
let err = loader
.load_resource(reference)
.expect_err("reference with NUL byte must error");
assert!(
matches!(
err,
LoaderError::InvalidUri { .. } | LoaderError::NoFetcherRegistered { .. }
),
"expected InvalidUri or NoFetcherRegistered, got {err:?}"
);
}
}