Skip to main content

vld/combinators/
discriminated_union.rs

1use serde_json::Value;
2
3use crate::error::{value_type_name, IssueCode, PathSegment, VldError};
4use crate::schema::DynSchema;
5use crate::schema::VldSchema;
6
7/// Entry in a discriminated union: a discriminator value and its associated schema.
8struct Variant {
9    discriminator_value: Value,
10    schema: Box<dyn DynSchema + Send + Sync>,
11}
12
13/// Discriminated union: chooses a schema based on a discriminator field value.
14///
15/// More efficient than a regular union because it looks up the correct variant
16/// by the discriminator field value instead of trying each schema in order.
17///
18/// Created via [`vld::discriminated_union()`](crate::discriminated_union).
19///
20/// # Example
21/// ```ignore
22/// let schema = vld::discriminated_union("type")
23///     .variant("dog", vld::object().field("type", vld::literal("dog")).field("bark", vld::boolean()))
24///     .variant("cat", vld::object().field("type", vld::literal("cat")).field("lives", vld::number().int()));
25/// ```
26pub struct ZDiscriminatedUnion {
27    discriminator: String,
28    variants: Vec<Variant>,
29}
30
31impl ZDiscriminatedUnion {
32    pub fn new(discriminator: impl Into<String>) -> Self {
33        Self {
34            discriminator: discriminator.into(),
35            variants: vec![],
36        }
37    }
38
39    /// Add a variant: when the discriminator field equals `value`, use `schema`.
40    pub fn variant<S: DynSchema + Send + Sync + 'static>(
41        mut self,
42        value: impl Into<Value>,
43        schema: S,
44    ) -> Self {
45        self.variants.push(Variant {
46            discriminator_value: value.into(),
47            schema: Box::new(schema),
48        });
49        self
50    }
51
52    /// Name of the discriminator field.
53    pub fn discriminator(&self) -> &str {
54        &self.discriminator
55    }
56
57    /// Variants as `(discriminator value, schema)` pairs, in insertion order.
58    pub fn variants(&self) -> impl Iterator<Item = (&Value, &dyn DynSchema)> {
59        self.variants
60            .iter()
61            .map(|v| (&v.discriminator_value, v.schema.as_ref() as &dyn DynSchema))
62    }
63
64    /// Add a string variant (convenience).
65    pub fn variant_str<S: DynSchema + Send + Sync + 'static>(self, value: &str, schema: S) -> Self {
66        self.variant(Value::String(value.to_string()), schema)
67    }
68}
69
70impl VldSchema for ZDiscriminatedUnion {
71    type Output = Value;
72
73    fn parse_value(&self, value: &Value) -> Result<Value, VldError> {
74        let obj = value.as_object().ok_or_else(|| {
75            VldError::from_code_with_value(
76                IssueCode::InvalidType {
77                    expected: "object".to_string(),
78                    received: value_type_name(value),
79                },
80                value,
81            )
82        })?;
83        let at_disc = |e: VldError| e.with_prefix(PathSegment::Field(self.discriminator.clone()));
84
85        let disc_value = obj
86            .get(&self.discriminator)
87            .ok_or_else(|| at_disc(VldError::from_code(IssueCode::MissingField)))?;
88
89        for variant in &self.variants {
90            if crate::primitives::json_eq(disc_value, &variant.discriminator_value) {
91                return variant.schema.dyn_parse(value);
92            }
93        }
94
95        let options = self
96            .variants
97            .iter()
98            .map(|v| v.discriminator_value.clone())
99            .collect();
100        Err(at_disc(VldError::from_code_with_value(
101            IssueCode::InvalidDiscriminator { options },
102            disc_value,
103        )))
104    }
105}