Skip to main content

openapiv3_resolve/resolved/
discriminator.rs

1use super::resolver::Resolver;
2use super::{NestedSchema, ResolvedSchemaKind};
3use crate::ResolveError;
4use indexmap::IndexMap;
5use openapiv3::{Discriminator, ReferenceOr, Schema, SchemaKind};
6
7/// [`Discriminator`] with every mapping value replaced by the schema it named.
8///
9/// A mapping value is either a `$ref` or a bare schema name. A value
10/// containing `#` is followed as a `$ref` (`#/components/schemas/Cat`, or a
11/// reference into another document, which fails exactly as the equivalent
12/// `$ref` would); any other value is the name of a schema under
13/// `#/components/schemas`, looked up as written.
14///
15/// On a `oneOf` or `anyOf` schema every mapping value must name one of the
16/// alternatives, and the entry is that alternative's very edge; a value that
17/// names any other schema fails with
18/// [`ResolveError::DiscriminatorMappingMismatch`]. Inline alternatives cannot
19/// be named, as the specification says. A schema that is not a `oneOf` or
20/// `anyOf` (the `allOf` inheritance pattern, where the mapping lives on the
21/// parent) has no alternatives to check against, so its mapping values are
22/// resolved like any other `$ref`. Entries are resolved in document order,
23/// and the first one that fails is the error reported.
24#[derive(Debug, PartialEq)]
25pub struct ResolvedDiscriminator {
26    /// See [`Discriminator::property_name`].
27    pub property_name: String,
28    /// See [`Discriminator::mapping`].
29    pub mapping: IndexMap<String, NestedSchema>,
30    /// See [`Discriminator::extensions`].
31    pub extensions: IndexMap<String, serde_json::Value>,
32}
33
34/// Resolves `discriminator`, which sits on a schema whose kind has already
35/// been resolved to `resolved` from `kind`.
36pub(super) fn resolve_discriminator(
37    cx: &mut Resolver<'_>,
38    discriminator: &Discriminator,
39    kind: &SchemaKind,
40    resolved: &ResolvedSchemaKind,
41) -> Result<ResolvedDiscriminator, ResolveError> {
42    // Destructured without `..` so that a field added upstream fails to
43    // compile here instead of being quietly dropped from the mirror.
44    let Discriminator {
45        property_name,
46        mapping,
47        extensions,
48    } = discriminator;
49    let alternatives = alternatives(cx, kind, resolved)?;
50    let mapping = mapping
51        .iter()
52        .map(|(value, target)| {
53            let target = MappingTarget::classify(target);
54            let edge = match &alternatives {
55                Some(alternatives) => {
56                    let schema = cx.schema_name(target)?;
57                    alternatives
58                        .iter()
59                        .find(|(name, _)| *name == schema)
60                        .map(|(_, edge)| edge.duplicate())
61                        .ok_or_else(|| ResolveError::DiscriminatorMappingMismatch {
62                            property_name: property_name.clone(),
63                            value: value.clone(),
64                            schema,
65                        })?
66                }
67                None => cx.nested_target(target)?,
68            };
69            Ok((value.clone(), edge))
70        })
71        .collect::<Result<_, _>>()?;
72    Ok(ResolvedDiscriminator {
73        property_name: property_name.clone(),
74        mapping,
75        extensions: extensions.clone(),
76    })
77}
78
79/// How one discriminator mapping value names its schema.
80///
81/// The specification allows either a schema name or a reference and does not
82/// say how to tell them apart. A component name can never contain `#`, so a
83/// `#` marks a reference; everything else is taken as a name, which keeps
84/// names outside the specification's grammar (`a/b`) reachable all the same.
85#[derive(Clone, Copy)]
86pub(super) enum MappingTarget<'a> {
87    /// A `$ref`, local or into another document.
88    Reference(&'a str),
89    /// The name of a schema under `#/components/schemas`.
90    Name(&'a str),
91}
92
93impl<'a> MappingTarget<'a> {
94    fn classify(target: &'a str) -> Self {
95        if target.contains('#') {
96            Self::Reference(target)
97        } else {
98            Self::Name(target)
99        }
100    }
101}
102
103/// The alternatives a mapping value may name, as the name each `$ref` finally
104/// resolves to paired with its resolved edge; `None` when the schema is not a
105/// `oneOf` or `anyOf` and so has no alternatives at all.
106fn alternatives<'a>(
107    cx: &Resolver<'_>,
108    kind: &'a SchemaKind,
109    resolved: &'a ResolvedSchemaKind,
110) -> Result<Option<Vec<(String, &'a NestedSchema)>>, ResolveError> {
111    let (Some(entries), Some(edges)) = (entries_of(kind), edges_of(resolved)) else {
112        return Ok(None);
113    };
114    entries
115        .into_iter()
116        .zip(edges)
117        // Inline alternatives have no name a mapping value could use.
118        .filter_map(|(entry, edge)| match entry {
119            ReferenceOr::Item(_) => None,
120            ReferenceOr::Reference { reference } => Some((reference, edge)),
121        })
122        .map(|(reference, edge)| Ok((cx.schema_name(MappingTarget::Reference(reference))?, edge)))
123        .collect::<Result<_, _>>()
124        .map(Some)
125}
126
127fn entries_of(kind: &SchemaKind) -> Option<Vec<&ReferenceOr<Schema>>> {
128    match kind {
129        SchemaKind::OneOf { one_of } => Some(one_of.iter().collect()),
130        SchemaKind::AnyOf { any_of } => Some(any_of.iter().collect()),
131        SchemaKind::Any(any) if !(any.one_of.is_empty() && any.any_of.is_empty()) => {
132            Some(any.one_of.iter().chain(&any.any_of).collect())
133        }
134        SchemaKind::Any(_)
135        | SchemaKind::Type(_)
136        | SchemaKind::AllOf { .. }
137        | SchemaKind::Not { .. } => None,
138    }
139}
140
141fn edges_of(resolved: &ResolvedSchemaKind) -> Option<Vec<&NestedSchema>> {
142    match resolved {
143        ResolvedSchemaKind::OneOf { one_of } => Some(one_of.iter().collect()),
144        ResolvedSchemaKind::AnyOf { any_of } => Some(any_of.iter().collect()),
145        ResolvedSchemaKind::Any(any) if !(any.one_of.is_empty() && any.any_of.is_empty()) => {
146            Some(any.one_of.iter().chain(&any.any_of).collect())
147        }
148        ResolvedSchemaKind::Any(_)
149        | ResolvedSchemaKind::Type(_)
150        | ResolvedSchemaKind::AllOf { .. }
151        | ResolvedSchemaKind::Not { .. } => None,
152    }
153}