use crate::common::helpers::validate_required_string;
use crate::common::reference::RefOr;
use crate::v3_2::schema::Schema;
use crate::v3_2::spec::Spec;
use crate::validation::{Context, ValidateWithContext};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Discriminator {
#[serde(rename = "propertyName")]
pub property_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub mapping: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "defaultMapping")]
pub default_mapping: Option<String>,
}
impl ValidateWithContext<Spec> for Discriminator {
fn validate_with_context(&self, ctx: &mut Context<Spec>, path: String) {
validate_required_string(&self.property_name, ctx, format!("{path}.propertyName"));
let resolve = |v: &str| -> String {
let is_uri_ref = v.contains('/') || v.starts_with('#') || v.contains(':');
if is_uri_ref {
v.to_owned()
} else {
format!("#/components/schemas/{v}")
}
};
if let Some(mapping) = &self.mapping {
for (k, v) in mapping {
RefOr::<Schema>::new_ref(resolve(v))
.validate_with_context(ctx, format!("{path}.mapping[{k}]"));
}
}
if let Some(v) = &self.default_mapping {
RefOr::<Schema>::new_ref(resolve(v))
.validate_with_context(ctx, format!("{path}.defaultMapping"));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::v3_2::schema::{ObjectSchema, SingleSchema};
use crate::validation::Context;
use crate::validation::Options;
use crate::validation::ValidationErrorsExt;
#[test]
fn round_trip_with_mapping() {
let json = serde_json::json!({
"propertyName": "type",
"mapping": {"cat": "Cat", "dog": "Dog"}
});
let d: Discriminator = serde_json::from_value(json.clone()).unwrap();
assert_eq!(d.property_name, "type");
assert_eq!(d.mapping.as_ref().unwrap().len(), 2);
assert_eq!(serde_json::to_value(&d).unwrap(), json);
}
#[test]
fn validate_empty_property_name_errors() {
let spec = Spec::default();
let mut ctx = Context::new(&spec, Options::new());
Discriminator::default().validate_with_context(&mut ctx, "d".to_owned());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("propertyName") && e.contains("must not be empty")),
"errors: {:?}",
ctx.errors
);
}
#[test]
fn mapping_resolves_against_components() {
let mut spec = Spec::default();
spec.define_schema(
"Cat",
Schema::Single(Box::new(SingleSchema::Object(ObjectSchema::default()))),
)
.unwrap();
let d = Discriminator {
property_name: "type".into(),
mapping: Some(BTreeMap::from([
("cat".to_owned(), "Cat".to_owned()),
("missing".to_owned(), "Missing".to_owned()),
])),
default_mapping: None,
};
let mut ctx = Context::new(&spec, Options::new());
d.validate_with_context(&mut ctx, "d".to_owned());
assert!(
ctx.errors.mentions("Missing"),
"expected missing schema error: {:?}",
ctx.errors
);
}
#[test]
fn mapping_uri_ref_used_directly() {
let mut spec = Spec::default();
spec.define_schema(
"Cat",
Schema::Single(Box::new(SingleSchema::Object(ObjectSchema::default()))),
)
.unwrap();
let d = Discriminator {
property_name: "type".into(),
mapping: Some(BTreeMap::from([
("cat".to_owned(), "#/components/schemas/Cat".to_owned()),
])),
default_mapping: None,
};
let mut ctx = Context::new(&spec, Options::new());
d.validate_with_context(&mut ctx, "d".to_owned());
assert!(
ctx.errors.is_empty(),
"URI ref in mapping should resolve: {:?}",
ctx.errors
);
}
#[test]
fn default_mapping_round_trip_and_resolution() {
let v = serde_json::json!({
"propertyName": "type",
"mapping": {"cat": "Cat"},
"defaultMapping": "Animal"
});
let d: Discriminator = serde_json::from_value(v.clone()).unwrap();
assert_eq!(d.default_mapping.as_deref(), Some("Animal"));
assert_eq!(serde_json::to_value(&d).unwrap(), v);
let spec = Spec::default();
let mut ctx = Context::new(&spec, Options::new());
d.validate_with_context(&mut ctx, "d".to_owned());
assert!(
ctx.errors
.iter()
.any(|e| e.contains("defaultMapping") && e.contains("Animal")),
"expected dangling defaultMapping target: {:?}",
ctx.errors
);
}
}