1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//! Discriminator Object
use crate::common::helpers::validate_required_string;
use crate::common::reference::RefOr;
use crate::v3_1::schema::Schema;
use crate::v3_1::spec::Spec;
use crate::validation::{Context, ValidateWithContext};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
/// When request bodies or response payloads may be one of a number of different schemas,
/// a discriminator object can be used to aid in serialization, deserialization, and validation.
/// The discriminator is a specific object in a schema which is used to inform the consumer of t
/// he specification of an alternative schema based on the value associated with it.
///
/// When using the discriminator, inline schemas will not be considered.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Default)]
pub struct Discriminator {
/// **Required** The name of the property in the payload that will hold the discriminator value.
#[serde(rename = "propertyName")]
pub property_name: String,
/// An object to hold mappings between payload values and schema names or references.
#[serde(skip_serializing_if = "Option::is_none")]
pub mapping: Option<BTreeMap<String, 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"));
if let Some(mapping) = &self.mapping {
for (k, v) in mapping {
// Per OAS 3.1, mapping values are EITHER component schema
// *names* (resolved against `#/components/schemas/<name>`)
// OR URI references per RFC 3986. We distinguish by the
// shape of valid component names: per
// `Components.<map>` keys, names match
// `^[a-zA-Z0-9._-]+$` — no `/`, no `#`, no `:`. So any
// value containing `/`, `#`, or `:` is treated as a URI
// reference and used as-is. Anything else is taken as a
// component schema name.
let is_uri_ref = v.contains('/') || v.starts_with('#') || v.contains(':');
let reference = if is_uri_ref {
v.clone()
} else {
format!("#/components/schemas/{v}")
};
let schema_ref = RefOr::<Schema>::new_ref(reference);
schema_ref.validate_with_context(ctx, format!("{path}.mapping[{k}]"));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::v3_1::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_uri_ref_used_as_is() {
// When a mapping value contains '/', '#', or ':' it is treated as a
// URI reference and used verbatim (not wrapped in "#/components/schemas/").
// This exercises the `is_uri_ref = true` branch at discriminator.rs:45.
let spec = Spec::default();
let d = Discriminator {
property_name: "type".into(),
mapping: Some(BTreeMap::from([
// URI with '/': used verbatim → resolves against spec (missing → error)
("cat".to_owned(), "#/components/schemas/Cat".to_owned()),
// URI with ':': used verbatim
(
"dog".to_owned(),
"https://example.com/schemas/Dog".to_owned(),
),
])),
};
let mut ctx = Context::new(&spec, Options::new());
d.validate_with_context(&mut ctx, "d".to_owned());
// Both are URI refs → validated as-is; Cat is missing from spec so error expected.
// The key thing is this exercises the v.clone() branch.
assert!(
ctx.errors.iter().any(|e| e.contains("Cat")),
"expected missing Cat error: {:?}",
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()),
])),
};
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
);
}
}