Skip to main content

siderite_validation/
change.rs

1//! [`Change`]: one field of a partial update, telling an absent field
2//! from `null`.
3
4use crate::context::ValidationContext;
5use crate::schema::{Schema, SchemaObject, SchemaRegistry};
6use crate::validate::Validate;
7use serde::{Deserialize, Deserializer};
8use serde_json::Value;
9
10/// A field of a partial-update (`PATCH`) body: absent, or a new value.
11///
12/// Mark the field `#[serde(default)]`: an absent field is
13/// [`Unchanged`](Self::Unchanged), a present one is [`Set`](Self::Set)
14/// with the value `T` reads from the input. `null` is therefore a value:
15/// `Change<Option<T>>` holds `Set(None)` for it (clear the column), while
16/// for a `T` that rejects `null` (`String`, numbers, ...) the body fails
17/// validation. Validation and the schema are `T`'s.
18///
19/// ```
20/// use serde::Deserialize;
21/// use siderite_validation::Change;
22///
23/// #[derive(Deserialize)]
24/// struct Patch {
25///     #[serde(default)]
26///     note: Change<Option<String>>,
27/// }
28///
29/// let absent: Patch = serde_json::from_str("{}").unwrap();
30/// assert_eq!(absent.note, Change::Unchanged);
31/// let cleared: Patch = serde_json::from_str(r#"{"note":null}"#).unwrap();
32/// assert_eq!(cleared.note, Change::Set(None));
33/// ```
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
35pub enum Change<T> {
36    /// The field was absent: keep the current value.
37    #[default]
38    Unchanged,
39    /// The field was present: the new value.
40    Set(T),
41}
42
43impl<T> Change<T> {
44    /// Store the new value in `target`, if there is one.
45    ///
46    /// Args:
47    ///   target: The value to replace.
48    pub fn apply(self, target: &mut T) {
49        if let Self::Set(value) = self {
50            *target = value;
51        }
52    }
53
54    /// The new value, if the field was present.
55    ///
56    /// Returns:
57    ///   `Some` for [`Set`](Self::Set), `None` for
58    ///   [`Unchanged`](Self::Unchanged).
59    pub fn into_option(self) -> Option<T> {
60        match self {
61            Self::Unchanged => None,
62            Self::Set(value) => Some(value),
63        }
64    }
65
66    /// Whether the field was present.
67    pub const fn is_set(&self) -> bool {
68        matches!(self, Self::Set(_))
69    }
70}
71
72impl<'de, T: Deserialize<'de>> Deserialize<'de> for Change<T> {
73    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
74        T::deserialize(d).map(Self::Set)
75    }
76}
77
78impl<T: Validate> Validate for Change<T> {
79    fn prepare(input: &mut Value, ctx: &mut ValidationContext) {
80        // Only present fields reach here: `null` is checked as `T` would.
81        T::prepare(input, ctx);
82    }
83
84    fn validate(&self, ctx: &mut ValidationContext) {
85        if let Self::Set(value) = self {
86            value.validate(ctx);
87        }
88    }
89}
90
91impl<T: Schema + 'static> Schema for Change<T> {
92    fn schema(registry: &mut SchemaRegistry) -> SchemaObject {
93        registry.subschema::<T>()
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    #![allow(clippy::unwrap_used)]
100
101    use super::*;
102
103    #[derive(Debug, Deserialize)]
104    struct Body {
105        #[serde(default)]
106        name: Change<String>,
107        #[serde(default)]
108        note: Change<Option<i32>>,
109    }
110
111    fn parse(text: &str) -> Result<Body, serde_json::Error> {
112        serde_json::from_str(text)
113    }
114
115    #[test]
116    fn absent_null_and_values() {
117        let absent = parse("{}").unwrap();
118        assert_eq!(absent.name, Change::Unchanged);
119        assert_eq!(absent.note, Change::Unchanged);
120        let set = parse(r#"{"name":"x","note":3}"#).unwrap();
121        assert_eq!(set.name, Change::Set("x".to_owned()));
122        assert_eq!(set.note, Change::Set(Some(3)));
123        let cleared = parse(r#"{"note":null}"#).unwrap();
124        assert_eq!(cleared.note, Change::Set(None));
125        assert!(parse(r#"{"name":null}"#).is_err());
126    }
127
128    #[test]
129    fn apply_and_accessors() {
130        let mut current = 1;
131        Change::Unchanged.apply(&mut current);
132        assert_eq!(current, 1);
133        Change::Set(2).apply(&mut current);
134        assert_eq!(current, 2);
135        assert_eq!(Change::Set(5).into_option(), Some(5));
136        assert_eq!(Change::<i32>::Unchanged.into_option(), None);
137        assert!(Change::Set(()).is_set());
138    }
139
140    #[test]
141    fn the_schema_is_the_inner_type() {
142        let mut registry = SchemaRegistry::default();
143        let inner = registry.subschema::<i64>();
144        let changed = Change::<i64>::schema(&mut registry);
145        assert_eq!(changed, inner);
146    }
147}