use serde::{Deserialize, Serialize};
use crate::common::helpers::{Context, PushError, ValidateWithContext};
use crate::common::reference::{ResolveError, ResolveReference};
use crate::validation::Options;
use std::collections::BTreeSet;
#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Ref {
#[serde(rename = "$ref")]
pub reference: String,
}
impl Ref {
pub fn new(reference: impl Into<String>) -> Self {
Ref {
reference: reference.into(),
}
}
pub 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");
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[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)
}
}
}
impl<D> RefOr<D> {
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 validate_with_context<T>(&self, ctx: &mut Context<T>, path: String)
where
T: ResolveReference<D>,
D: ValidateWithContext<T>,
{
match self {
RefOr::Ref(r) => {
r.validate_with_context(ctx, path.clone());
if ctx.visit(r.reference.clone()) {
match self.get_item(ctx.spec) {
Ok(d) => {
d.validate_with_context(ctx, r.reference.clone());
}
Err(ResolveError::NotFound(reference)) => {
ctx.error(path, format_args!(".$ref: `{reference}` not found"));
}
Err(e @ ResolveError::ExternalUnsupported(_)) => {
if !ctx.is_option(Options::IgnoreExternalReferences) {
ctx.error(path, format_args!(".$ref: {e}"));
}
}
}
}
}
RefOr::Item(d) => d.validate_with_context(ctx, path),
}
}
}
pub fn resolve_in_map<'a, T, D>(
spec: &'a T,
reference: &str,
prefix: &str,
map: &'a Option<std::collections::BTreeMap<String, RefOr<D>>>,
) -> Option<&'a D>
where
T: ResolveReference<D>,
{
let map = map.as_ref()?;
let mut current = reference;
let mut visited = 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 serde_json::json;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
struct Foo {
pub foo: String,
}
#[test]
fn ref_only_serializes_dollar_ref() {
let r = RefOr::<Foo>::new_ref("#/components/schemas/Foo");
assert_eq!(
serde_json::to_value(&r).unwrap(),
json!({"$ref": "#/components/schemas/Foo"}),
);
}
#[test]
fn deserialize_rejects_v3_1_fields() {
let r = serde_json::from_value::<RefOr<Foo>>(json!({
"$ref": "#/components/schemas/Foo",
"summary": "should be rejected",
}));
assert!(
r.is_err(),
"should not silently accept v3.1 summary on v3.0 Ref"
);
}
#[test]
fn deserialize_ref() {
let r: RefOr<Foo> =
serde_json::from_value(json!({"$ref": "#/components/schemas/Foo"})).unwrap();
assert!(matches!(r, RefOr::Ref(ref rr) if rr.reference == "#/components/schemas/Foo"));
}
#[test]
fn schema_with_no_type_parses_as_inline_object() {
let r: RefOr<crate::v3_0::schema::Schema> =
serde_json::from_value(json!({"properties": {}})).expect("must parse");
assert!(matches!(r, RefOr::Item(_)), "expected inline Item form");
}
#[test]
fn schema_ref_with_extras_does_not_fall_back_to_inline() {
let r = serde_json::from_value::<RefOr<crate::v3_0::schema::Schema>>(json!({
"$ref": "#/components/schemas/Foo",
"description": "this v3.1 sibling is rejected",
}));
assert!(
r.is_err(),
"ref form must fail strictly when 3.1 sibling fields are present, even with permissive ObjectSchema"
);
}
#[test]
fn schema_ref_rejects_v3_1_sibling_fields() {
let r = serde_json::from_value::<RefOr<crate::v3_0::schema::Schema>>(json!({
"$ref": "#/components/schemas/Foo",
"description": "should be rejected",
}));
assert!(
r.is_err(),
"schema refs must not fall back to inline schemas when v3.1 sibling fields are present"
);
}
}