use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use thiserror::Error;
use crate::loader::{Loader, LoaderError};
use crate::validation::{Context, Options, PushError, ValidateWithContext};
pub trait ResolveReference<D> {
fn resolve_reference(&self, reference: &str) -> Option<&D>;
}
#[derive(Debug, Error)]
pub enum ResolveError {
#[error("reference `{0}` not found")]
NotFound(String),
#[error("resolving of an external reference `{0}` is not supported")]
ExternalUnsupported(String),
#[error("failed to resolve external reference `{reference}`")]
External {
reference: String,
#[source]
source: LoaderError,
},
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[serde(untagged)]
pub enum RefOr<T> {
Ref(Ref),
Item(T),
}
impl<'de, T> Deserialize<'de> for RefOr<T>
where
T: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
let has_ref = matches!(&value, serde_json::Value::Object(m) if m.contains_key("$ref"));
if has_ref {
Ref::deserialize(value)
.map(RefOr::Ref)
.map_err(serde::de::Error::custom)
} else {
T::deserialize(value)
.map(RefOr::Item)
.map_err(serde::de::Error::custom)
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
#[serde(deny_unknown_fields)]
pub struct Ref {
#[serde(rename = "$ref")]
pub reference: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl<D> RefOr<D> {
pub(crate) fn validate_with_context<T>(&self, ctx: &mut Context<T>, path: String)
where
T: ResolveReference<D>,
D: ValidateWithContext<T> + 'static + Clone + DeserializeOwned,
{
match self {
RefOr::Ref(r) => {
r.validate_with_context::<T, D>(ctx, path.clone());
if !ctx.visit(r.reference.clone()) {
return;
}
if r.reference.starts_with("#/") {
match ctx.spec.resolve_reference(&r.reference) {
Some(d) => d.validate_with_context(ctx, r.reference.clone()),
None => ctx.error(path, format_args!(".$ref: `{}` not found", r.reference)),
}
return;
}
if ctx.is_option(Options::IgnoreExternalReferences) {
return;
}
let resolved = ctx
.loader
.as_deref_mut()
.map(|l| l.resolve_reference_as::<D>(&r.reference));
match resolved {
Some(Ok(d)) => d.validate_with_context(ctx, r.reference.clone()),
Some(Err(source)) => ctx.error(
path,
format_args!(
".$ref: failed to resolve external reference `{}`: {source}",
r.reference,
),
),
None => ctx.error(
path,
format_args!(
".$ref: resolving of an external reference `{}` is not supported",
r.reference,
),
),
}
}
RefOr::Item(d) => {
d.validate_with_context(ctx, path);
}
}
}
pub fn new_ref(reference: impl Into<String>) -> Self {
RefOr::Ref(Ref::new(reference))
}
pub fn new_item(item: D) -> Self {
RefOr::Item(item)
}
pub fn get_item<'a, T>(&'a self, spec: &'a T) -> Result<&'a D, ResolveError>
where
T: ResolveReference<D>,
{
match self {
RefOr::Item(d) => Ok(d),
RefOr::Ref(r) => {
if r.reference.starts_with("#/") {
match spec.resolve_reference(&r.reference) {
Some(d) => Ok(d),
None => Err(ResolveError::NotFound(r.reference.clone())),
}
} else {
Err(ResolveError::ExternalUnsupported(r.reference.clone()))
}
}
}
}
pub fn get_item_with_loader<'a, T>(
&'a self,
spec: &'a T,
loader: &mut Loader,
) -> Result<Cow<'a, D>, ResolveError>
where
T: ResolveReference<D>,
D: 'static + Clone + DeserializeOwned,
{
match self {
RefOr::Item(d) => Ok(Cow::Borrowed(d)),
RefOr::Ref(r) => {
if r.reference.starts_with("#/") {
spec.resolve_reference(&r.reference)
.map(Cow::Borrowed)
.ok_or_else(|| ResolveError::NotFound(r.reference.clone()))
} else {
loader
.resolve_reference_as::<D>(&r.reference)
.map(Cow::Owned)
.map_err(|source| ResolveError::External {
reference: r.reference.clone(),
source,
})
}
}
}
}
}
impl Ref {
pub(crate) fn validate_with_context<T, D>(&self, ctx: &mut Context<T>, path: String)
where
T: ResolveReference<D>,
D: ValidateWithContext<T>,
{
if self.reference.is_empty() {
ctx.error(path, ".$ref: must not be empty");
}
}
pub fn new(reference: impl Into<String>) -> Self {
Ref {
reference: reference.into(),
..Default::default()
}
}
}
pub fn resolve_in_map<'a, T, D>(
spec: &'a T,
reference: &str,
prefix: &str,
map: &'a Option<BTreeMap<String, RefOr<D>>>,
) -> Option<&'a D>
where
T: ResolveReference<D>,
{
let map = map.as_ref()?;
let mut current = reference;
let mut visited: BTreeSet<&str> = BTreeSet::new();
loop {
let key = current.strip_prefix(prefix)?;
let item = map.get(key)?;
match item {
RefOr::Item(d) => return Some(d),
RefOr::Ref(r) => {
if !r.reference.starts_with(prefix) {
return item.get_item(spec).ok();
}
if !visited.insert(r.reference.as_str()) {
return None;
}
current = &r.reference;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::loader::{JsonFileFetcher, ResourceFetcher};
use crate::validation::ValidationErrorsExt;
use serde_json::Value;
use std::fs;
use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
struct Foo {
pub foo: String,
}
struct PetSpec;
impl ResolveReference<Foo> for PetSpec {
fn resolve_reference(&self, _reference: &str) -> Option<&Foo> {
None
}
}
#[test]
fn test_ref_or_foo_serialize() {
assert_eq!(
serde_json::to_value(RefOr::new_item(Foo {
foo: String::from("bar"),
}))
.unwrap(),
serde_json::json!({
"foo": "bar"
}),
"serialize item",
);
assert_eq!(
serde_json::to_value(RefOr::Ref::<Foo>(Ref {
reference: String::from("#/components/schemas/Foo"),
..Default::default()
}))
.unwrap(),
serde_json::json!({
"$ref": "#/components/schemas/Foo"
}),
"serialize ref",
);
}
#[test]
fn test_ref_or_foo_deserialize() {
assert_eq!(
serde_json::from_value::<RefOr<Foo>>(serde_json::json!({
"foo":"bar",
}))
.unwrap(),
RefOr::new_item(Foo {
foo: String::from("bar"),
}),
"deserialize item",
);
assert_eq!(
serde_json::from_value::<RefOr<Foo>>(serde_json::json!({
"$ref":"#/components/schemas/Foo",
}))
.unwrap(),
RefOr::Ref(Ref {
reference: String::from("#/components/schemas/Foo"),
..Default::default()
}),
"deserialize ref",
);
}
#[test]
fn ref_with_unknown_sibling_is_rejected() {
let r = serde_json::from_value::<RefOr<Foo>>(serde_json::json!({
"$ref": "#/components/schemas/Foo",
"typo": "unexpected sibling",
}));
assert!(
r.is_err(),
"$ref form must fail strictly when unknown siblings are present"
);
}
#[test]
fn ref_with_summary_and_description_is_accepted() {
let r: RefOr<Foo> = serde_json::from_value(serde_json::json!({
"$ref": "#/components/schemas/Foo",
"summary": "s",
"description": "d",
}))
.unwrap();
match r {
RefOr::Ref(rr) => {
assert_eq!(rr.reference, "#/components/schemas/Foo");
assert_eq!(rr.summary.as_deref(), Some("s"));
assert_eq!(rr.description.as_deref(), Some("d"));
}
RefOr::Item(_) => panic!("expected Ref variant"),
}
}
#[test]
fn get_item_with_loader_external_resolves_via_loader() {
let dir = std::env::temp_dir().join(format!(
"roas-refor-loader-{}-{}",
std::process::id(),
"external"
));
fs::create_dir_all(&dir).unwrap();
let file = dir.join("pets.json");
fs::write(
&file,
br#"{"components":{"schemas":{"Pet":{"foo":"bar"}}}}"#,
)
.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 r = RefOr::<Foo>::new_ref(url.to_string());
let spec = PetSpec;
let resolved = r.get_item_with_loader(&spec, &mut loader).unwrap();
match resolved {
Cow::Owned(foo) => assert_eq!(foo.foo, "bar"),
Cow::Borrowed(_) => panic!("expected owned value from loader"),
}
fs::remove_file(file).unwrap();
fs::remove_dir(dir).unwrap();
}
#[derive(Clone, Default)]
struct FailingFetcher;
impl ResourceFetcher for FailingFetcher {
fn fetch(&mut self, uri: &Url) -> Result<Value, LoaderError> {
Err(LoaderError::Parse {
uri: uri.as_str().to_string(),
source: serde_json::from_str::<Value>("not json").unwrap_err(),
})
}
}
#[test]
fn get_item_with_loader_propagates_loader_error() {
let mut loader = Loader::new();
loader.register_fetcher("https://", FailingFetcher);
let r = RefOr::<Foo>::new_ref("https://example.test/pets.json#/Foo");
let spec = PetSpec;
let err = r.get_item_with_loader(&spec, &mut loader).unwrap_err();
match err {
ResolveError::External { reference, source } => {
assert_eq!(reference, "https://example.test/pets.json#/Foo");
assert!(matches!(source, LoaderError::Parse { .. }));
}
other => panic!("expected External, got {other:?}"),
}
}
#[test]
fn get_item_with_loader_internal_uses_spec() {
struct InlineSpec(Foo);
impl ResolveReference<Foo> for InlineSpec {
fn resolve_reference(&self, reference: &str) -> Option<&Foo> {
if reference == "#/components/schemas/Foo" {
Some(&self.0)
} else {
None
}
}
}
let mut loader = Loader::new();
let r = RefOr::<Foo>::new_ref("#/components/schemas/Foo");
let spec = InlineSpec(Foo {
foo: "from-spec".into(),
});
let resolved = r.get_item_with_loader(&spec, &mut loader).unwrap();
match resolved {
Cow::Borrowed(foo) => assert_eq!(foo.foo, "from-spec"),
Cow::Owned(_) => panic!("expected borrowed value from spec"),
}
}
#[test]
fn get_item_with_loader_inline_item_returns_borrowed() {
let mut loader = Loader::new();
let inline = Foo {
foo: "inline".into(),
};
let r = RefOr::<Foo>::new_item(inline);
let spec = PetSpec;
let resolved = r.get_item_with_loader(&spec, &mut loader).unwrap();
match resolved {
Cow::Borrowed(foo) => assert_eq!(foo.foo, "inline"),
Cow::Owned(_) => panic!("inline item must come back borrowed"),
}
}
#[test]
fn get_item_with_loader_internal_ref_missing_in_spec_is_not_found() {
let mut loader = Loader::new();
let r = RefOr::<Foo>::new_ref("#/components/schemas/Missing");
let spec = PetSpec; let err = r.get_item_with_loader(&spec, &mut loader).unwrap_err();
assert!(matches!(err, ResolveError::NotFound(_)));
}
#[derive(Default)]
struct FooSpec {
foo: Option<Foo>,
}
impl ResolveReference<Foo> for FooSpec {
fn resolve_reference(&self, reference: &str) -> Option<&Foo> {
if reference == "#/components/schemas/Foo" {
self.foo.as_ref()
} else {
None
}
}
}
impl ValidateWithContext<FooSpec> for Foo {
fn validate_with_context(&self, ctx: &mut Context<FooSpec>, path: String) {
if self.foo.is_empty() {
ctx.error(path, "foo must not be empty");
}
}
}
#[test]
fn validate_with_context_loaderless_external_ref_emits_not_supported_error() {
let spec = FooSpec::default();
let r = RefOr::<Foo>::new_ref("https://example.test/foo.json#/Foo");
let mut ctx = Context::new(&spec, Options::new());
r.validate_with_context(&mut ctx, "#.x".into());
assert!(
ctx.errors
.mentions_all(&["not supported", "https://example.test/foo.json"]),
"expected `not supported` error, got: {:?}",
ctx.errors,
);
}
#[test]
fn validate_with_context_loaderless_external_ref_is_silenced_under_ignore() {
let spec = FooSpec::default();
let r = RefOr::<Foo>::new_ref("https://example.test/foo.json#/Foo");
let mut ctx = Context::new(&spec, Options::IgnoreExternalReferences.only());
r.validate_with_context(&mut ctx, "#.x".into());
assert!(
ctx.errors.is_empty(),
"IgnoreExternalReferences must silence loader-less external errors: {:?}",
ctx.errors,
);
}
#[test]
fn validate_with_context_internal_not_found_emits_error() {
let spec = FooSpec::default(); let r = RefOr::<Foo>::new_ref("#/components/schemas/Foo");
let mut ctx = Context::new(&spec, Options::new());
r.validate_with_context(&mut ctx, "#.x".into());
assert!(
ctx.errors.mentions("not found"),
"expected `not found` error, got: {:?}",
ctx.errors,
);
}
#[test]
fn validate_with_context_internal_hit_recurses_into_resolved_value() {
let spec = FooSpec {
foo: Some(Foo { foo: String::new() }),
};
let r = RefOr::<Foo>::new_ref("#/components/schemas/Foo");
let mut ctx = Context::new(&spec, Options::new());
r.validate_with_context(&mut ctx, "#.x".into());
assert!(
ctx.errors.mentions("must not be empty"),
"expected recursive `must not be empty` error, got: {:?}",
ctx.errors,
);
}
#[test]
fn validate_with_context_inline_item_recurses() {
let spec = FooSpec::default();
let r = RefOr::<Foo>::new_item(Foo { foo: String::new() });
let mut ctx = Context::new(&spec, Options::new());
r.validate_with_context(&mut ctx, "#.x".into());
assert!(
ctx.errors.mentions("must not be empty"),
"inline item validation must propagate, got: {:?}",
ctx.errors,
);
}
#[test]
fn validate_with_context_visited_ref_is_not_revisited() {
let spec = FooSpec {
foo: Some(Foo { foo: String::new() }),
};
let r = RefOr::<Foo>::new_ref("#/components/schemas/Foo");
let mut ctx = Context::new(&spec, Options::new());
r.validate_with_context(&mut ctx, "#.x".into());
let first = ctx.errors.len();
r.validate_with_context(&mut ctx, "#.y".into());
assert_eq!(
ctx.errors.len(),
first,
"second walk of the same ref must not add new errors"
);
}
#[test]
fn ref_validate_with_context_emits_must_not_be_empty_for_empty_ref() {
let spec = FooSpec::default();
let r = Ref::new("");
let mut ctx = Context::new(&spec, Options::new());
Ref::validate_with_context::<FooSpec, Foo>(&r, &mut ctx, "#.x".into());
assert!(
ctx.errors.mentions("must not be empty"),
"empty `$ref` must error: {:?}",
ctx.errors,
);
}
}