Skip to main content

standout_dispatch/
contract.rs

1//! The versioned contract surface (`docs/topics/stability.md`).
2//!
3//! A type that implements [`ContractSurface`] names the version of the shape
4//! it serializes to. `T::envelope(self)` wraps a value as an [`Envelope`],
5//! which serializes as `{"schema_version": N, "data": <T>}`, so a consumer of
6//! `--output json` can tell a shape change from a data change without parsing
7//! anything else. A framework-owned document (the diagnostic, the list view,
8//! the help document) implements the trait too but carries `schema_version`
9//! as a top-level key beside its own fields instead of nesting under `data`.
10
11use serde::ser::SerializeStruct;
12use serde::{Serialize, Serializer};
13
14pub trait ContractSurface {
15    const SCHEMA_VERSION: u32;
16
17    fn envelope(self) -> Envelope<Self>
18    where
19        Self: Sized,
20    {
21        Envelope { data: self }
22    }
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub struct Envelope<T> {
27    data: T,
28}
29
30impl<T: ContractSurface> Envelope<T> {
31    pub fn new(data: T) -> Self {
32        Self { data }
33    }
34
35    pub const fn schema_version(&self) -> u32 {
36        T::SCHEMA_VERSION
37    }
38
39    pub fn data(&self) -> &T {
40        &self.data
41    }
42
43    pub fn into_data(self) -> T {
44        self.data
45    }
46}
47
48impl<T: ContractSurface + Serialize> Serialize for Envelope<T> {
49    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
50        let mut envelope = serializer.serialize_struct("Envelope", 2)?;
51        envelope.serialize_field("schema_version", &T::SCHEMA_VERSION)?;
52        envelope.serialize_field("data", &self.data)?;
53        envelope.end()
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[derive(Serialize)]
62    struct Listing {
63        items: Vec<&'static str>,
64    }
65
66    impl ContractSurface for Listing {
67        const SCHEMA_VERSION: u32 = 3;
68    }
69
70    #[test]
71    fn the_envelope_stamps_the_version_before_the_data() {
72        let json = serde_json::to_string(&Listing { items: vec!["a"] }.envelope()).unwrap();
73        assert_eq!(json, r#"{"schema_version":3,"data":{"items":["a"]}}"#);
74    }
75
76    #[test]
77    fn the_envelope_hands_the_data_back() {
78        let envelope = Envelope::new(Listing {
79            items: vec!["a", "b"],
80        });
81        assert_eq!(envelope.schema_version(), 3);
82        assert_eq!(envelope.data().items.len(), 2);
83        assert_eq!(envelope.into_data().items, ["a", "b"]);
84    }
85}