1use std::{error::Error, fmt};
4
5use serde::{Deserialize, Serialize, de::DeserializeOwned};
6use serde_json::Value;
7
8#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
10#[serde(rename_all = "camelCase")]
11pub struct ProtocolIdentity {
12 pub name: String,
14 pub major: u32,
16 pub revision: String,
18 #[serde(default, skip_serializing_if = "Vec::is_empty")]
20 pub features: Vec<String>,
21}
22
23impl ProtocolIdentity {
24 #[must_use]
26 pub fn new(name: impl Into<String>, major: u32, revision: impl Into<String>) -> Self {
27 Self {
28 name: name.into(),
29 major,
30 revision: revision.into(),
31 features: Vec::new(),
32 }
33 }
34
35 #[must_use]
37 pub fn with_features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
38 self.features = features.into_iter().map(Into::into).collect();
39 self
40 }
41
42 pub fn validate(&self, expected_name: &str, expected_major: u32) -> Result<(), ProtocolError> {
48 if self.name != expected_name {
49 return Err(ProtocolError::UnexpectedProtocol {
50 expected: expected_name.to_string(),
51 actual: self.name.clone(),
52 });
53 }
54 if self.major != expected_major {
55 return Err(ProtocolError::UnsupportedMajor {
56 protocol: self.name.clone(),
57 expected: expected_major,
58 actual: self.major,
59 });
60 }
61 Ok(())
62 }
63}
64
65#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
67pub struct VersionedEnvelope<T> {
68 pub schema: String,
70 pub version: u32,
72 pub payload: T,
74}
75
76impl<T> VersionedEnvelope<T> {
77 #[must_use]
79 pub fn new(schema: impl Into<String>, version: u32, payload: T) -> Self {
80 Self {
81 schema: schema.into(),
82 version,
83 payload,
84 }
85 }
86}
87
88pub trait VersionedRecord: Sized {
90 const SCHEMA: &'static str;
92 const VERSION: u32 = 1;
94 const ALLOW_BARE_V0: bool = false;
96
97 fn decode_version(version: u32, payload: Value) -> Result<Self, VersionedRecordError>
106 where
107 Self: DeserializeOwned,
108 {
109 if version == 0 || version != Self::VERSION {
110 return Err(VersionedRecordError::UnsupportedVersion {
111 schema: Self::SCHEMA,
112 supported: Self::VERSION,
113 actual: version,
114 });
115 }
116 serde_json::from_value(payload).map_err(VersionedRecordError::Json)
117 }
118
119 fn decode_bare_v0(payload: Value) -> Result<Self, VersionedRecordError>
125 where
126 Self: DeserializeOwned,
127 {
128 if !Self::ALLOW_BARE_V0 {
129 return Err(VersionedRecordError::BareV0Unsupported {
130 schema: Self::SCHEMA,
131 });
132 }
133 serde_json::from_value(payload).map_err(VersionedRecordError::Json)
134 }
135}
136
137pub fn to_versioned_json<T>(value: &T) -> Result<String, serde_json::Error>
143where
144 T: Serialize + VersionedRecord,
145{
146 serde_json::to_string(&VersionedEnvelope::new(T::SCHEMA, T::VERSION, value))
147}
148
149pub fn to_versioned_value<T>(value: &T) -> Result<Value, serde_json::Error>
155where
156 T: Serialize + VersionedRecord,
157{
158 serde_json::to_value(VersionedEnvelope::new(T::SCHEMA, T::VERSION, value))
159}
160
161pub fn from_versioned_json<T>(input: &str) -> Result<T, VersionedRecordError>
170where
171 T: DeserializeOwned + VersionedRecord,
172{
173 let value = serde_json::from_str(input).map_err(VersionedRecordError::Json)?;
174 from_versioned_value(value)
175}
176
177pub fn from_versioned_value<T>(value: Value) -> Result<T, VersionedRecordError>
183where
184 T: DeserializeOwned + VersionedRecord,
185{
186 let looks_enveloped = value
187 .as_object()
188 .is_some_and(|object| object.contains_key("schema") || object.contains_key("version"));
189 if !looks_enveloped {
190 return T::decode_bare_v0(value);
191 }
192
193 let envelope = serde_json::from_value::<VersionedEnvelope<Value>>(value)
194 .map_err(VersionedRecordError::Json)?;
195 if envelope.schema != T::SCHEMA {
196 return Err(VersionedRecordError::WrongSchema {
197 expected: T::SCHEMA,
198 actual: envelope.schema,
199 });
200 }
201 T::decode_version(envelope.version, envelope.payload)
202}
203
204#[derive(Clone, Debug, Eq, PartialEq)]
206pub enum ProtocolError {
207 UnexpectedProtocol {
209 expected: String,
211 actual: String,
213 },
214 UnsupportedMajor {
216 protocol: String,
218 expected: u32,
220 actual: u32,
222 },
223}
224
225impl fmt::Display for ProtocolError {
226 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
227 match self {
228 Self::UnexpectedProtocol { expected, actual } => {
229 write!(formatter, "expected protocol {expected}, received {actual}")
230 }
231 Self::UnsupportedMajor {
232 protocol,
233 expected,
234 actual,
235 } => write!(
236 formatter,
237 "unsupported {protocol} major {actual}; supported major is {expected}"
238 ),
239 }
240 }
241}
242
243impl Error for ProtocolError {}
244
245#[derive(Debug)]
247pub enum VersionedRecordError {
248 Json(serde_json::Error),
250 BareV0Unsupported {
252 schema: &'static str,
254 },
255 WrongSchema {
257 expected: &'static str,
259 actual: String,
261 },
262 UnsupportedVersion {
264 schema: &'static str,
266 supported: u32,
268 actual: u32,
270 },
271}
272
273impl fmt::Display for VersionedRecordError {
274 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
275 match self {
276 Self::Json(error) => write!(formatter, "invalid durable JSON: {error}"),
277 Self::BareV0Unsupported { schema } => {
278 write!(
279 formatter,
280 "bare v0 JSON is not supported for durable schema {schema}"
281 )
282 }
283 Self::WrongSchema { expected, actual } => {
284 write!(
285 formatter,
286 "expected durable schema {expected}, received {actual}"
287 )
288 }
289 Self::UnsupportedVersion {
290 schema,
291 supported,
292 actual,
293 } => write!(
294 formatter,
295 "unsupported {schema} version {actual}; supported version is {supported}"
296 ),
297 }
298 }
299}
300
301impl Error for VersionedRecordError {
302 fn source(&self) -> Option<&(dyn Error + 'static)> {
303 match self {
304 Self::Json(error) => Some(error),
305 Self::BareV0Unsupported { .. }
306 | Self::WrongSchema { .. }
307 | Self::UnsupportedVersion { .. } => None,
308 }
309 }
310}