Skip to main content

sc_command/
envelope.rs

1//! Response envelope `{version, ok, data, error}`.
2
3use serde::de::{self, DeserializeOwned};
4use serde::ser::SerializeMap;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use serde_json::Value;
7
8use crate::error::OpError;
9use crate::exact::{require_exact_keys, required};
10use crate::{DISCRIMINANT_CODE, ENVELOPE_VERSION};
11
12/// sc-ai-cli response envelope.
13///
14/// Success carries `data` and writes `error` as JSON `null`. Failure carries
15/// [`OpError`] and writes `data` as JSON `null`. Constructors stamp
16/// [`ENVELOPE_VERSION`]. Deserialization keeps the `version` string from the
17/// input. An `ok` flag that disagrees with the other field, or an object that
18/// does not have exactly the four keys, is rejected with
19/// [`DISCRIMINANT_CODE`] in the error.
20#[derive(Debug, Clone, PartialEq)]
21pub enum Envelope<T> {
22    /// `ok` is true and `data` is `T`.
23    Success {
24        /// Contract version read or written for this value.
25        version: String,
26        /// Successful payload. JSON `null` is a valid `T` such as `()` or `None`.
27        data: T,
28    },
29    /// `ok` is false and `error` is the operation error.
30    Failure {
31        /// Contract version read or written for this value.
32        version: String,
33        /// Operation error.
34        error: OpError,
35    },
36}
37
38impl<T> Envelope<T> {
39    /// Builds a success envelope at [`ENVELOPE_VERSION`].
40    #[must_use]
41    pub fn success(data: T) -> Self {
42        Self::Success {
43            version: ENVELOPE_VERSION.to_owned(),
44            data,
45        }
46    }
47
48    /// Builds a failure envelope at [`ENVELOPE_VERSION`].
49    #[must_use]
50    pub fn failure(error: OpError) -> Self {
51        Self::Failure {
52            version: ENVELOPE_VERSION.to_owned(),
53            error,
54        }
55    }
56
57    /// Contract version stored on this value.
58    #[must_use]
59    pub fn version(&self) -> &str {
60        match self {
61            Self::Success { version, .. } | Self::Failure { version, .. } => version,
62        }
63    }
64
65    /// Reports whether this value is the success case.
66    #[must_use]
67    pub fn is_ok(&self) -> bool {
68        matches!(self, Self::Success { .. })
69    }
70
71    /// Returns the payload when this value is the success case.
72    #[must_use]
73    pub fn data(&self) -> Option<&T> {
74        match self {
75            Self::Success { data, .. } => Some(data),
76            Self::Failure { .. } => None,
77        }
78    }
79
80    /// Returns the error when this value is the failure case.
81    #[must_use]
82    pub fn error(&self) -> Option<&OpError> {
83        match self {
84            Self::Failure { error, .. } => Some(error),
85            Self::Success { .. } => None,
86        }
87    }
88
89    /// Consumes the envelope and returns the success payload or the operation error.
90    ///
91    /// `version` is not part of the result. A caller that still needs the
92    /// contract version reads [`version`](Self::version) before calling this
93    /// method.
94    ///
95    /// # Errors
96    ///
97    /// Returns `Err` when the envelope is the failure case. The error is the
98    /// [`OpError`] the envelope carried.
99    ///
100    /// # Examples
101    ///
102    /// ```
103    /// use sc_command::Envelope;
104    ///
105    /// let envelope = Envelope::success("ready");
106    /// assert_eq!(envelope.into_result(), Ok("ready"));
107    /// ```
108    pub fn into_result(self) -> Result<T, OpError> {
109        match self {
110            Self::Success { data, .. } => Ok(data),
111            Self::Failure { error, .. } => Err(error),
112        }
113    }
114}
115
116impl<T> From<Result<T, OpError>> for Envelope<T> {
117    fn from(result: Result<T, OpError>) -> Self {
118        match result {
119            Ok(data) => Self::success(data),
120            Err(error) => Self::failure(error),
121        }
122    }
123}
124
125impl<T> Serialize for Envelope<T>
126where
127    T: Serialize,
128{
129    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
130    where
131        S: Serializer,
132    {
133        let mut map = serializer.serialize_map(Some(4))?;
134        match self {
135            Self::Success { version, data } => {
136                map.serialize_entry("version", version)?;
137                map.serialize_entry("ok", &true)?;
138                map.serialize_entry("data", data)?;
139                map.serialize_entry("error", &None::<OpError>)?;
140            }
141            Self::Failure { version, error } => {
142                map.serialize_entry("version", version)?;
143                map.serialize_entry("ok", &false)?;
144                map.serialize_entry("data", &None::<()>)?;
145                map.serialize_entry("error", error)?;
146            }
147        }
148        map.end()
149    }
150}
151
152impl<'de, T> Deserialize<'de> for Envelope<T>
153where
154    T: DeserializeOwned,
155{
156    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
157    where
158        D: Deserializer<'de>,
159    {
160        let value = Value::deserialize(deserializer)?;
161        envelope_from_value(value).map_err(de::Error::custom)
162    }
163}
164
165fn envelope_from_value<T>(value: Value) -> Result<Envelope<T>, String>
166where
167    T: DeserializeOwned,
168{
169    let map = match value {
170        Value::Object(map) => map,
171        _ => {
172            return Err(format!(
173                "{DISCRIMINANT_CODE}: envelope must be a JSON object"
174            ));
175        }
176    };
177    require_exact_keys(&map, &["version", "ok", "data", "error"])?;
178    let version = match required(&map, "version")? {
179        Value::String(version) => version.clone(),
180        _ => {
181            return Err(format!(
182                "{DISCRIMINANT_CODE}: version must be a JSON string"
183            ));
184        }
185    };
186    let ok = match required(&map, "ok")? {
187        Value::Bool(ok) => *ok,
188        _ => {
189            return Err(format!("{DISCRIMINANT_CODE}: ok must be a JSON boolean"));
190        }
191    };
192    let data = required(&map, "data")?;
193    let error = required(&map, "error")?;
194    if ok {
195        if !error.is_null() {
196            return Err(format!(
197                "{DISCRIMINANT_CODE}: ok is true and error is not null"
198            ));
199        }
200        let data = T::deserialize(data).map_err(|error| format!("{DISCRIMINANT_CODE}: {error}"))?;
201        Ok(Envelope::Success { version, data })
202    } else {
203        if !data.is_null() {
204            return Err(format!(
205                "{DISCRIMINANT_CODE}: ok is false and data is not null"
206            ));
207        }
208        if error.is_null() {
209            return Err(format!(
210                "{DISCRIMINANT_CODE}: ok is false and error is null"
211            ));
212        }
213        let error =
214            OpError::deserialize(error).map_err(|error| format!("{DISCRIMINANT_CODE}: {error}"))?;
215        Ok(Envelope::Failure { version, error })
216    }
217}