use serde::de::{self, DeserializeOwned, IntoDeserializer, MapAccess, SeqAccess, Visitor};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::marker::PhantomData;
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(Box<Ref>),
Item(T),
}
const REF_FIELDS: &[&str] = &["$ref", "summary", "description"];
struct RefOrVisitor<T>(PhantomData<fn() -> T>);
impl<'de, T> Visitor<'de> for RefOrVisitor<T>
where
T: Deserialize<'de>,
{
type Value = RefOr<T>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a Reference Object or an inline component")
}
fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
T::deserialize(v.into_deserializer()).map(RefOr::Item)
}
fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
T::deserialize(v.into_deserializer()).map(RefOr::Item)
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
T::deserialize(v.into_deserializer()).map(RefOr::Item)
}
fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> {
T::deserialize(v.into_deserializer()).map(RefOr::Item)
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
T::deserialize(v.into_deserializer()).map(RefOr::Item)
}
fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
T::deserialize(v.into_deserializer()).map(RefOr::Item)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
T::deserialize(().into_deserializer()).map(RefOr::Item)
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
T::deserialize(de::value::SeqAccessDeserializer::new(seq)).map(RefOr::Item)
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let Some(first_key) = map.next_key::<String>()? else {
return T::deserialize(de::value::MapAccessDeserializer::new(map)).map(RefOr::Item);
};
if first_key == "$ref" {
let reference: String = map.next_value()?;
let mut summary: Option<String> = None;
let mut description: Option<String> = None;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"$ref" => return Err(de::Error::duplicate_field("$ref")),
"summary" => {
if summary.is_some() {
return Err(de::Error::duplicate_field("summary"));
}
summary = Some(map.next_value()?);
}
"description" => {
if description.is_some() {
return Err(de::Error::duplicate_field("description"));
}
description = Some(map.next_value()?);
}
_ => return Err(de::Error::unknown_field(key.as_str(), REF_FIELDS)),
}
}
return Ok(RefOr::Ref(Box::new(Ref {
reference,
summary,
description,
})));
}
let mut entries = serde_json::Map::new();
entries.insert(first_key, map.next_value()?);
while let Some(key) = map.next_key::<String>()? {
entries.insert(key, map.next_value()?);
}
let value = serde_json::Value::Object(entries);
if value.as_object().is_some_and(|m| m.contains_key("$ref")) {
Ref::deserialize(value)
.map(|r| RefOr::Ref(Box::new(r)))
.map_err(de::Error::custom)
} else {
T::deserialize(value)
.map(RefOr::Item)
.map_err(de::Error::custom)
}
}
}
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>,
{
deserializer.deserialize_any(RefOrVisitor(PhantomData))
}
}
#[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>,
}
const _: () = assert!(
std::mem::size_of::<RefOr<u8>>() < std::mem::size_of::<Ref>(),
"RefOr::Ref must stay boxed",
);
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(Box::new(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()
}
}
}
impl<D> crate::merge::MergeWithContext for RefOr<D>
where
D: crate::merge::MergeWithContext,
{
fn merge_with_context(
&mut self,
other: Self,
ctx: &mut crate::merge::MergeContext,
path: &mut String,
) {
if ctx.errored {
return;
}
use crate::merge::ConflictKind;
match (self, other) {
(RefOr::Item(base), RefOr::Item(incoming)) => {
base.merge_with_context(incoming, ctx, path);
}
(slot @ RefOr::Ref(_), RefOr::Ref(incoming_ref)) => {
let RefOr::Ref(base_ref) = slot else {
unreachable!()
};
if base_ref.reference == incoming_ref.reference {
base_ref.merge_with_context(*incoming_ref, ctx, path);
} else if ctx.should_take_incoming(path, ConflictKind::RefReplaced) {
*slot = RefOr::Ref(incoming_ref);
}
}
(slot, incoming) => {
if ctx.should_take_incoming(path, ConflictKind::RefVsValue) {
*slot = incoming;
}
}
}
}
}
impl crate::merge::MergeWithContext for Ref {
fn merge_with_context(
&mut self,
other: Self,
ctx: &mut crate::merge::MergeContext,
path: &mut String,
) {
use crate::common::merge::merge_opt_scalar;
use crate::merge::ConflictKind;
merge_opt_scalar(
&mut self.summary,
other.summary,
ctx,
path,
".summary",
ConflictKind::ScalarOverridden,
);
merge_opt_scalar(
&mut self.description,
other.description,
ctx,
path,
".description",
ConflictKind::ScalarOverridden,
);
}
}
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>(Box::new(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(Box::new(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_detected_when_not_the_first_key() {
let r: RefOr<Foo> = serde_json::from_value(serde_json::json!({
"description": "d",
"$ref": "#/components/schemas/Foo",
}))
.unwrap();
match r {
RefOr::Ref(rr) => {
assert_eq!(rr.reference, "#/components/schemas/Foo");
assert_eq!(rr.description.as_deref(), Some("d"));
}
RefOr::Item(_) => panic!("expected Ref variant"),
}
}
#[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 ref_fast_and_slow_paths_agree_on_a_full_ref() {
let fast: RefOr<Foo> =
serde_json::from_str(r##"{"$ref":"#/x","summary":"s","description":"d"}"##).unwrap();
let slow: RefOr<Foo> =
serde_json::from_str(r##"{"summary":"s","$ref":"#/x","description":"d"}"##).unwrap();
assert_eq!(fast, slow, "first-key and non-first-key $ref must agree");
match fast {
RefOr::Ref(r) => {
assert_eq!(r.reference, "#/x");
assert_eq!(r.summary.as_deref(), Some("s"));
assert_eq!(r.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,
);
}
#[test]
fn get_item_external_unsupported_returns_error() {
let spec = PetSpec;
let r = RefOr::<Foo>::new_ref("https://example.test/foo.json#/Foo");
let err = r.get_item(&spec).unwrap_err();
assert!(
matches!(err, ResolveError::ExternalUnsupported(_)),
"expected ExternalUnsupported, got {err:?}"
);
assert!(err.to_string().contains("external reference"));
}
#[test]
fn get_item_internal_not_found_returns_error() {
let spec = PetSpec; let r = RefOr::<Foo>::new_ref("#/components/schemas/Missing");
let err = r.get_item(&spec).unwrap_err();
assert!(matches!(err, ResolveError::NotFound(_)));
assert!(err.to_string().contains("not found"));
}
#[test]
fn resolve_in_map_cross_map_ref_delegates_to_spec_resolver() {
use std::collections::BTreeMap;
struct ConcreteSpec(Foo);
impl ResolveReference<Foo> for ConcreteSpec {
fn resolve_reference(&self, reference: &str) -> Option<&Foo> {
if reference == "#/components/schemas/Concrete" {
Some(&self.0)
} else {
None
}
}
}
let concrete_foo = Foo {
foo: "concrete".into(),
};
let spec = ConcreteSpec(concrete_foo);
let mut map: BTreeMap<String, RefOr<Foo>> = BTreeMap::new();
map.insert(
"Alias".into(),
RefOr::new_ref("#/components/schemas/Concrete"),
);
let map_opt = Some(map);
let result = crate::common::reference::resolve_in_map(
&spec,
"#/things/Alias",
"#/things/",
&map_opt,
);
assert_eq!(result.map(|f| f.foo.as_str()), Some("concrete"));
}
#[test]
fn resolve_in_map_cycle_returns_none() {
use std::collections::BTreeMap;
struct EmptySpec;
impl ResolveReference<Foo> for EmptySpec {
fn resolve_reference(&self, _reference: &str) -> Option<&Foo> {
None
}
}
let mut map: BTreeMap<String, RefOr<Foo>> = BTreeMap::new();
map.insert("A".into(), RefOr::new_ref("#/things/B"));
map.insert("B".into(), RefOr::new_ref("#/things/A"));
let map_opt = Some(map);
let result = crate::common::reference::resolve_in_map(
&EmptySpec,
"#/things/A",
"#/things/",
&map_opt,
);
assert!(result.is_none(), "cycle detection must return None");
}
#[test]
fn ref_or_visitor_scalar_deserializations() {
use serde_json::Value;
let r: RefOr<Value> = serde_json::from_str("true").unwrap();
assert_eq!(r, RefOr::Item(Value::Bool(true)));
let r: RefOr<Value> = serde_json::from_str("false").unwrap();
assert_eq!(r, RefOr::Item(Value::Bool(false)));
let r: RefOr<Value> = serde_json::from_str("42").unwrap();
assert_eq!(r, RefOr::Item(Value::Number(42_u64.into())));
let r: RefOr<Value> = serde_json::from_str("-1").unwrap();
assert_eq!(r, RefOr::Item(Value::Number((-1_i64).into())));
let r: RefOr<Value> = serde_json::from_str("2.5").unwrap();
match r {
RefOr::Item(Value::Number(n)) => {
let v = n.as_f64().unwrap();
assert!((v - 2.5).abs() < 1e-10, "expected ~2.5, got {v}");
}
other => panic!("expected Item(Number), got {other:?}"),
}
let r: RefOr<Value> = serde_json::from_str(r#""hello""#).unwrap();
assert_eq!(r, RefOr::Item(Value::String("hello".into())));
let r: RefOr<Value> = serde_json::from_str("null").unwrap();
assert_eq!(r, RefOr::Item(Value::Null));
let r: RefOr<Value> = serde_json::from_str("[1,2,3]").unwrap();
assert_eq!(
r,
RefOr::Item(Value::Array(vec![
Value::Number(1_u64.into()),
Value::Number(2_u64.into()),
Value::Number(3_u64.into()),
]))
);
let r: RefOr<Value> = serde_json::from_str("[]").unwrap();
assert_eq!(r, RefOr::Item(Value::Array(vec![])));
}
#[test]
fn ref_or_visitor_visit_string_via_owned_deserializer() {
use serde::Deserialize;
use serde::de::IntoDeserializer;
use serde::de::value::StringDeserializer;
use serde_json::Value;
let des: StringDeserializer<serde_json::Error> = "owned".to_owned().into_deserializer();
let r: RefOr<Value> = RefOr::deserialize(des).unwrap();
assert_eq!(r, RefOr::Item(Value::String("owned".into())));
}
#[test]
fn ref_or_visitor_expecting_message_via_error() {
use serde::Deserialize;
use serde::de::IntoDeserializer;
use serde::de::value::U64Deserializer;
let des: U64Deserializer<serde_json::Error> = 99_u64.into_deserializer();
let r = Foo::deserialize(des);
assert!(r.is_err(), "Foo should not deserialize from u64");
}
#[test]
fn ref_duplicate_summary_is_rejected() {
let r = serde_json::from_str::<RefOr<Foo>>(
r##"{"$ref":"#/x","summary":"first","summary":"second"}"##,
);
assert!(r.is_err(), "duplicate summary must be rejected");
}
#[test]
fn ref_duplicate_description_is_rejected() {
let r = serde_json::from_str::<RefOr<Foo>>(
r##"{"$ref":"#/x","description":"first","description":"second"}"##,
);
assert!(r.is_err(), "duplicate description must be rejected");
}
#[test]
fn resolve_error_display_messages() {
let e = ResolveError::NotFound("my-ref".into());
assert!(e.to_string().contains("not found"));
let e = ResolveError::ExternalUnsupported("http://x".into());
assert!(e.to_string().contains("not supported"));
use crate::loader::LoaderError;
let e = ResolveError::External {
reference: "http://x/y".into(),
source: LoaderError::NoFetcherRegistered {
uri: "http://x/y".into(),
},
};
assert!(
e.to_string()
.contains("failed to resolve external reference")
);
let src = std::error::Error::source(&e).expect("External must have a source");
assert!(src.to_string().contains("no fetcher"));
}
use crate::merge::{ConflictKind, MergeContext, MergeOptions, MergeWithContext, Resolution};
impl MergeWithContext for Foo {
fn merge_with_context(&mut self, other: Self, ctx: &mut MergeContext, path: &mut String) {
if self.foo != other.foo
&& ctx.should_take_incoming(path, ConflictKind::ScalarOverridden)
{
self.foo = other.foo;
}
}
}
fn merge_ctx(opts: enumset::EnumSet<MergeOptions>) -> MergeContext {
MergeContext::new(opts)
}
#[test]
fn refor_item_vs_ref_replaces_with_ref_vs_value_kind() {
let mut base: RefOr<Foo> = RefOr::new_item(Foo { foo: "x".into() });
let mut ctx = merge_ctx(MergeOptions::new());
let mut path = "#".to_owned();
base.merge_with_context(RefOr::new_ref("#/foo/A".to_owned()), &mut ctx, &mut path);
assert!(matches!(base, RefOr::Ref(_)));
assert_eq!(ctx.conflicts.len(), 1);
assert_eq!(ctx.conflicts[0].kind, ConflictKind::RefVsValue);
}
#[test]
fn ref_summary_description_merge_independently() {
let mut base = Ref {
reference: "#/c/A".into(),
summary: Some("base sum".into()),
description: None,
};
let incoming = Ref {
reference: "#/c/A".into(),
summary: None,
description: Some("inc desc".into()),
};
let mut ctx = merge_ctx(MergeOptions::new());
let mut path = "#.ref".to_owned();
base.merge_with_context(incoming, &mut ctx, &mut path);
assert_eq!(base.summary.as_deref(), Some("base sum"));
assert_eq!(base.description.as_deref(), Some("inc desc"));
assert!(ctx.conflicts.is_empty());
}
#[test]
fn ref_summary_collision_records_conflict() {
let mut base = Ref {
reference: "#/c/A".into(),
summary: Some("a".into()),
description: None,
};
let mut ctx = merge_ctx(MergeOptions::new());
let mut path = "#.ref".to_owned();
base.merge_with_context(
Ref {
reference: "#/c/A".into(),
summary: Some("b".into()),
description: None,
},
&mut ctx,
&mut path,
);
assert_eq!(base.summary.as_deref(), Some("b"));
assert_eq!(ctx.conflicts.len(), 1);
}
#[test]
fn ref_or_base_wins_records_resolution_base() {
let mut base: RefOr<Foo> = RefOr::new_ref("#/A");
let mut ctx = merge_ctx(MergeOptions::BaseWins.only());
let mut path = "#".to_owned();
base.merge_with_context(RefOr::new_ref("#/B".to_owned()), &mut ctx, &mut path);
match base {
RefOr::Ref(r) => assert_eq!(r.reference, "#/A"),
_ => panic!("expected Ref"),
}
assert_eq!(ctx.conflicts[0].resolution, Resolution::Base);
}
#[test]
fn ref_or_errored_entry_short_circuits() {
let mut base: RefOr<Foo> = RefOr::new_item(Foo { foo: "x".into() });
let mut ctx = merge_ctx(MergeOptions::new());
ctx.errored = true;
let mut path = "#".to_owned();
base.merge_with_context(
RefOr::new_item(Foo { foo: "y".into() }),
&mut ctx,
&mut path,
);
match base {
RefOr::Item(f) => assert_eq!(f.foo, "x"),
_ => panic!(),
}
assert!(ctx.conflicts.is_empty());
}
}