use crate::validation::{Context, ValidateWithContext};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Deserializer, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Reusable {
pub reference: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<serde_json::Value>,
}
impl ValidateWithContext for Reusable {
fn validate_with_context(&self, ctx: &mut Context) {
ctx.require_non_empty("reference", &self.reference);
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[serde(untagged)]
pub enum ReusableOr<T> {
Reusable(Reusable),
Item(T),
}
impl<'de, T> Deserialize<'de> for ReusableOr<T>
where
T: DeserializeOwned,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
if value.get("reference").is_some() {
serde_json::from_value(value)
.map(ReusableOr::Reusable)
.map_err(serde::de::Error::custom)
} else {
serde_json::from_value(value)
.map(ReusableOr::Item)
.map_err(serde::de::Error::custom)
}
}
}
impl<T: ValidateWithContext> ValidateWithContext for ReusableOr<T> {
fn validate_with_context(&self, ctx: &mut Context) {
match self {
ReusableOr::Reusable(r) => r.validate_with_context(ctx),
ReusableOr::Item(t) => t.validate_with_context(ctx),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use enumset::EnumSet;
use serde_json::json;
#[derive(Debug, Deserialize, Default)]
struct Demo {
name: String,
}
impl ValidateWithContext for Demo {
fn validate_with_context(&self, ctx: &mut Context) {
ctx.require_non_empty("name", &self.name);
}
}
#[test]
fn reusable_round_trips() {
let r: Reusable =
serde_json::from_value(json!({ "reference": "$components.parameters.foo" })).unwrap();
assert_eq!(r.reference, "$components.parameters.foo");
assert!(r.value.is_none());
}
#[test]
fn reusable_or_picks_reusable_for_reference_key() {
let v: ReusableOr<Demo> = serde_json::from_value(
json!({ "reference": "$components.parameters.foo", "value": 1 }),
)
.unwrap();
match v {
ReusableOr::Reusable(r) => {
assert_eq!(r.reference, "$components.parameters.foo");
assert_eq!(r.value, Some(json!(1)));
}
ReusableOr::Item(_) => panic!("expected reusable variant"),
}
}
#[test]
fn reusable_or_picks_item_for_concrete_object() {
let v: ReusableOr<Demo> = serde_json::from_value(json!({ "name": "petId" })).unwrap();
match v {
ReusableOr::Item(p) => assert_eq!(p.name, "petId"),
ReusableOr::Reusable(_) => panic!("expected item variant"),
}
}
#[test]
fn malformed_item_surfaces_inner_error_not_opaque_variant_error() {
let err = serde_json::from_value::<ReusableOr<Demo>>(json!({ "other": 1 })).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("missing field"), "got: {msg}");
assert!(!msg.contains("did not match any variant"), "got: {msg}");
}
#[test]
fn round_trips_through_yaml() {
let v: ReusableOr<Demo> = serde_yaml_ng::from_str("name: petId\n").unwrap();
assert!(matches!(v, ReusableOr::Item(_)));
let r: ReusableOr<Demo> =
serde_yaml_ng::from_str("reference: $components.parameters.foo\n").unwrap();
assert!(matches!(r, ReusableOr::Reusable(_)));
}
#[test]
fn validate_reusable_rejects_empty_reference() {
let mut c = Context::with_path(EnumSet::empty(), "#.parameters[0]");
let v: ReusableOr<Demo> = ReusableOr::Reusable(Reusable::default());
v.validate_with_context(&mut c);
assert!(
c.errors
.iter()
.any(|e| e == "#.parameters[0].reference: must not be empty")
);
}
#[test]
fn validate_item_delegates_to_inner() {
let mut c = Context::with_path(EnumSet::empty(), "#.parameters[0]");
let v: ReusableOr<Demo> = ReusableOr::Item(Demo::default());
v.validate_with_context(&mut c);
assert!(
c.errors
.iter()
.any(|e| e == "#.parameters[0].name: must not be empty")
);
}
}