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}