use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::String;
use foundation_errstacks::{ErrorTrace, IntoErrorTrace};
use derive_more::{Display, Error};
use serde_json::Value;
#[derive(Debug, Display, Error)]
#[display("failed to resolve external reference: {uri}")]
pub struct ResolveError {
pub uri: String,
}
impl ResolveError {
#[must_use]
pub fn new(uri: impl Into<String>) -> Self {
Self { uri: uri.into() }
}
}
pub trait JsonResolver {
fn resolve(&self, uri: &str) -> Result<Value, ErrorTrace<ResolveError>>;
}
#[derive(Debug, Clone, Copy)]
pub struct NoopResolver;
impl JsonResolver for NoopResolver {
fn resolve(&self, uri: &str) -> Result<Value, ErrorTrace<ResolveError>> {
Err(ResolveError::new(uri)
.into_error_trace()
.attach(format!("no resolver configured for: {uri}")))
}
}
#[derive(Debug, Clone)]
pub struct MapResolver {
schemas: BTreeMap<String, Value>,
}
impl MapResolver {
#[must_use]
pub fn new() -> Self {
Self {
schemas: BTreeMap::new(),
}
}
pub fn insert(&mut self, uri: impl Into<String>, schema: Value) -> &mut Self {
self.schemas.insert(uri.into(), schema);
self
}
#[must_use]
pub fn from_pairs(iter: impl IntoIterator<Item = (String, Value)>) -> Self {
Self {
schemas: BTreeMap::from_iter(iter),
}
}
}
impl Default for MapResolver {
fn default() -> Self {
Self::new()
}
}
impl JsonResolver for MapResolver {
fn resolve(&self, uri: &str) -> Result<Value, ErrorTrace<ResolveError>> {
self.schemas.get(uri).cloned().ok_or_else(|| {
ResolveError::new(uri)
.into_error_trace()
.attach(format!("not found in map resolver: {uri}"))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_noop_resolver_always_fails() {
let resolver = NoopResolver;
let result = resolver.resolve("https://example.com/schema.json");
assert!(result.is_err());
}
#[test]
fn test_map_resolver_found() {
let mut resolver = MapResolver::new();
resolver.insert("https://example.com/a.json", json!({"type": "string"}));
let result = resolver.resolve("https://example.com/a.json");
assert_eq!(result.unwrap(), json!({"type": "string"}));
}
#[test]
fn test_map_resolver_not_found() {
let resolver = MapResolver::new();
let result = resolver.resolve("https://example.com/missing.json");
assert!(result.is_err());
}
#[test]
fn test_map_resolver_from_pairs() {
let resolver = MapResolver::from_pairs(vec![
("https://a.json".into(), json!({"type": "string"})),
("https://b.json".into(), json!({"type": "number"})),
]);
assert_eq!(
resolver.resolve("https://a.json").unwrap(),
json!({"type": "string"})
);
assert_eq!(
resolver.resolve("https://b.json").unwrap(),
json!({"type": "number"})
);
assert!(resolver.resolve("https://c.json").is_err());
}
#[test]
fn test_resolve_error_display() {
let err = ResolveError::new("https://example.com/test.json");
let msg = format!("{err}");
assert!(msg.contains("https://example.com/test.json"));
}
}