Skip to main content

rustrade_execution/
stored.rs

1//! Reading back a value saved in a versioned format, so that one saved by another release is
2//! refused with the version it was written in rather than with a parse error.
3
4use serde::Deserialize;
5use thiserror::Error;
6
7/// A value that carries the version of the format it was written in.
8///
9/// A field added within a version takes `#[serde(default)]`, so a value written before the field
10/// existed still loads in a self-describing format such as JSON. A change an older value cannot
11/// be read under bumps [`VERSION`](Self::VERSION). An implementer whose value holds another
12/// versioned value must bump its own version whenever the inner one's rises, so that its version
13/// alone says whether it reads: nothing checks this for it.
14pub trait FormatVersion {
15    /// The format version this release writes, and the only one it reads.
16    const VERSION: u32;
17
18    /// The format version this value was written in.
19    fn format_version(&self) -> u32;
20}
21
22/// A saved `T`, read whatever format version it was written in: see
23/// [`into_current`](Self::into_current).
24///
25/// Deserialising a `T` itself fails in the deserialiser on a value whose shape a later format
26/// changed, with an error that does not say why. A `Stored<T>` reads such a value as
27/// [`Unread`](Self::Unread), keeping only its top-level `version` field, so the version it was
28/// written in can still be reported.
29///
30/// # Formats
31/// Needs a self-describing format, such as JSON, because it tries `T` first and then the
32/// `version` field alone, which a format like bincode cannot. In such a format deserialise `T`
33/// itself: whatever restores it then checks [`FormatVersion::format_version`].
34///
35/// It buffers the whole value while it reads it, so reading one takes about twice its memory.
36#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
37#[serde(untagged)]
38pub enum Stored<T> {
39    /// The value read as a `T`.
40    Read(T),
41    /// A value that does not read as a `T`, written in format version `version`.
42    Unread {
43        /// The value's top-level `version` field.
44        version: u32,
45    },
46}
47
48impl<T: FormatVersion> Stored<T> {
49    /// The value, if it was written in the format version this release reads.
50    ///
51    /// # Errors
52    /// - [`StoredFormatError::UnsupportedVersion`] if it was written in another version, whether
53    ///   or not its shape still reads as a `T`.
54    /// - [`StoredFormatError::Malformed`] if it claims this release's version but does not read as
55    ///   a `T`, such as a value edited by hand. Deserialise a `T` itself to learn why.
56    pub fn into_current(self) -> Result<T, StoredFormatError> {
57        let found = match self {
58            Self::Read(value) if value.format_version() == T::VERSION => return Ok(value),
59            Self::Read(value) => value.format_version(),
60            Self::Unread { version } if version == T::VERSION => {
61                return Err(StoredFormatError::Malformed { version });
62            }
63            Self::Unread { version } => version,
64        };
65        Err(StoredFormatError::UnsupportedVersion {
66            found,
67            supported: T::VERSION,
68        })
69    }
70}
71
72/// Why a [`Stored`] value is not one this release can use.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
74#[non_exhaustive]
75pub enum StoredFormatError {
76    /// The value was written in a format version this release does not read.
77    #[error("saved in format version {found}; this release reads version {supported}")]
78    UnsupportedVersion {
79        /// The version the value was written in.
80        found: u32,
81        /// The version this release reads.
82        supported: u32,
83    },
84
85    /// The value claims the format version this release reads, but does not read in it.
86    #[error(
87        "saved in format version {version}, which this release reads, but it does not read in \
88         that format"
89    )]
90    Malformed {
91        /// The version the value claims, which is this release's.
92        version: u32,
93    },
94}
95
96#[cfg(test)]
97#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
98mod tests {
99    use super::*;
100    use serde::Serialize;
101
102    #[derive(Debug, PartialEq, Deserialize, Serialize)]
103    struct Saved {
104        version: u32,
105        quantity: u32,
106        #[serde(default)]
107        note: String,
108    }
109
110    impl FormatVersion for Saved {
111        const VERSION: u32 = 3;
112
113        fn format_version(&self) -> u32 {
114            self.version
115        }
116    }
117
118    fn read(json: serde_json::Value) -> Result<Saved, StoredFormatError> {
119        serde_json::from_value::<Stored<Saved>>(json)
120            .unwrap()
121            .into_current()
122    }
123
124    #[test]
125    fn a_value_in_the_current_version_reads_and_one_without_a_later_field_takes_its_default() {
126        let saved = Saved {
127            version: 3,
128            quantity: 7,
129            note: "kept".into(),
130        };
131        assert_eq!(read(serde_json::to_value(&saved).unwrap()), Ok(saved));
132        assert_eq!(
133            read(serde_json::json!({ "version": 3, "quantity": 7 })),
134            Ok(Saved {
135                version: 3,
136                quantity: 7,
137                note: String::new(),
138            })
139        );
140    }
141
142    #[test]
143    fn a_value_in_another_version_is_refused_with_it_whatever_its_shape() {
144        let unsupported = Err(StoredFormatError::UnsupportedVersion {
145            found: 4,
146            supported: 3,
147        });
148        // Still reads as a `Saved`.
149        assert_eq!(
150            read(serde_json::json!({ "version": 4, "quantity": 7 })),
151            unsupported
152        );
153        // Does not.
154        assert_eq!(
155            read(serde_json::json!({ "version": 4, "quantity": "seven", "units": [] })),
156            unsupported
157        );
158    }
159
160    #[test]
161    fn a_value_claiming_the_current_version_in_another_shape_is_malformed() {
162        assert_eq!(
163            read(serde_json::json!({ "version": 3, "quantity": "seven" })),
164            Err(StoredFormatError::Malformed { version: 3 })
165        );
166    }
167
168    #[test]
169    fn a_value_without_a_version_is_not_read() {
170        assert!(
171            serde_json::from_value::<Stored<Saved>>(serde_json::json!({ "quantity": 7 })).is_err()
172        );
173    }
174}