Skip to main content

conversation_api/execution/
facade_response.rs

1//! Invocation outcomes, independent of the state of the resource being queried.
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum ResponseDetail {
8    #[default]
9    Full,
10    Essential,
11}
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum FacadeStatus {
16    Success,
17    Partial,
18    Error,
19    Cancelled,
20}
21
22#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(deny_unknown_fields)]
24pub struct FacadeProblem {
25    pub code: String,
26    pub message: String,
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub details: Option<Value>,
29}
30
31#[derive(Clone, Debug, PartialEq, Serialize)]
32#[serde(untagged)]
33pub enum Content {
34    Data { data: Value },
35    Text { text: String },
36}
37
38/// The public response has exactly one content representation on success/partial,
39/// an error on failure, and no content on cancellation. Constructors and strict
40/// deserialization enforce this invariant on both sides of the wire.
41#[derive(Clone, Debug, PartialEq, Serialize)]
42#[serde(tag = "status", rename_all = "snake_case")]
43pub enum FacadeResponse {
44    Success {
45        #[serde(flatten)]
46        content: Content,
47        #[serde(skip_serializing_if = "Vec::is_empty")]
48        warnings: Vec<FacadeProblem>,
49    },
50    Partial {
51        #[serde(flatten)]
52        content: Content,
53        #[serde(skip_serializing_if = "Vec::is_empty")]
54        warnings: Vec<FacadeProblem>,
55    },
56    Error {
57        error: FacadeProblem,
58    },
59    Cancelled {
60        #[serde(skip_serializing_if = "Vec::is_empty")]
61        warnings: Vec<FacadeProblem>,
62    },
63}
64
65impl FacadeResponse {
66    pub fn cancelled() -> Self {
67        Self::Cancelled { warnings: vec![] }
68    }
69    pub fn success(data: Value) -> Self {
70        Self::Success {
71            content: Content::Data { data },
72            warnings: vec![],
73        }
74    }
75    pub fn partial(data: Value) -> Self {
76        Self::Partial {
77            content: Content::Data { data },
78            warnings: vec![],
79        }
80    }
81    pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
82        Self::Error {
83            error: FacadeProblem {
84                code: code.into(),
85                message: message.into(),
86                details: None,
87            },
88        }
89    }
90    pub fn with_details(mut self, details: Value) -> Self {
91        if let Self::Error { error } = &mut self {
92            error.details = Some(details);
93        }
94        self
95    }
96    pub fn status(&self) -> FacadeStatus {
97        match self {
98            Self::Success { .. } => FacadeStatus::Success,
99            Self::Partial { .. } => FacadeStatus::Partial,
100            Self::Error { .. } => FacadeStatus::Error,
101            Self::Cancelled { .. } => FacadeStatus::Cancelled,
102        }
103    }
104    pub fn is_error(&self) -> bool {
105        matches!(self, Self::Partial { .. } | Self::Error { .. })
106    }
107    pub fn data(&self) -> Option<&Value> {
108        match self {
109            Self::Success {
110                content: Content::Data { data },
111                ..
112            }
113            | Self::Partial {
114                content: Content::Data { data },
115                ..
116            } => Some(data),
117            _ => None,
118        }
119    }
120    pub fn data_mut(&mut self) -> Option<&mut Value> {
121        match self {
122            Self::Success {
123                content: Content::Data { data },
124                ..
125            }
126            | Self::Partial {
127                content: Content::Data { data },
128                ..
129            } => Some(data),
130            _ => None,
131        }
132    }
133    /// Presentation never changes execution outcome or discards warnings.
134    pub fn with_text(self, text: String) -> Self {
135        match self {
136            Self::Success { warnings, .. } => Self::Success {
137                content: Content::Text { text },
138                warnings,
139            },
140            Self::Partial { warnings, .. } => Self::Partial {
141                content: Content::Text { text },
142                warnings,
143            },
144            terminal => terminal,
145        }
146    }
147    pub fn with_warning(mut self, warning: FacadeProblem) -> Self {
148        match &mut self {
149            Self::Success { warnings, .. }
150            | Self::Partial { warnings, .. }
151            | Self::Cancelled { warnings } => warnings.push(warning),
152            _ => {}
153        }
154        self
155    }
156}
157
158impl<'de> Deserialize<'de> for FacadeResponse {
159    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
160        use serde::de::Error;
161        let value = Value::deserialize(deserializer)?;
162        let mut fields = value
163            .as_object()
164            .cloned()
165            .ok_or_else(|| D::Error::custom("Facade response must be an object"))?;
166        let status = fields
167            .remove("status")
168            .ok_or_else(|| D::Error::custom("Missing Facade status"))?;
169        let response = match status.as_str() {
170            Some("success" | "partial") => {
171                let content = match (fields.remove("data"), fields.remove("text")) {
172                    (Some(data), None) => Content::Data { data },
173                    (None, Some(Value::String(text))) => Content::Text { text },
174                    _ => return Err(D::Error::custom("Exactly one of data or text is required")),
175                };
176                let warnings = fields
177                    .remove("warnings")
178                    .map(serde_json::from_value)
179                    .transpose()
180                    .map_err(D::Error::custom)?
181                    .unwrap_or_default();
182                if status == "success" {
183                    Self::Success { content, warnings }
184                } else {
185                    Self::Partial { content, warnings }
186                }
187            }
188            Some("error") => Self::Error {
189                error: serde_json::from_value(
190                    fields
191                        .remove("error")
192                        .ok_or_else(|| D::Error::custom("Missing Facade error"))?,
193                )
194                .map_err(D::Error::custom)?,
195            },
196            Some("cancelled") => Self::Cancelled {
197                warnings: fields
198                    .remove("warnings")
199                    .map(serde_json::from_value)
200                    .transpose()
201                    .map_err(D::Error::custom)?
202                    .unwrap_or_default(),
203            },
204            _ => return Err(D::Error::custom("Unknown Facade status")),
205        };
206        if !fields.is_empty() {
207            return Err(D::Error::custom("Unknown Facade response field"));
208        }
209        Ok(response)
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use serde_json::json;
217    #[test]
218    fn strict_response_roundtrip() {
219        for value in [
220            json!({"status":"success","data":null}),
221            json!({"status":"success","text":"items: []"}),
222            json!({"status":"partial","data":{"done":["a"],"failed":["b"]}}),
223            json!({"status":"error","error":{"code":"STALE","message":"Changed"}}),
224            json!({"status":"cancelled"}),
225        ] {
226            let parsed: FacadeResponse = serde_json::from_value(value.clone()).unwrap();
227            assert_eq!(serde_json::to_value(parsed).unwrap(), value);
228        }
229        for value in [
230            json!({"status":"success"}),
231            json!({"status":"success","data":{},"text":"bad"}),
232            json!({"status":"cancelled","data":{}}),
233            json!({"status":"error","error":{"message":"bad"}}),
234            json!({"status":"done","data":{}}),
235        ] {
236            assert!(serde_json::from_value::<FacadeResponse>(value).is_err());
237        }
238    }
239    #[test]
240    fn resource_failure_is_not_invocation_failure() {
241        let result = FacadeResponse::success(json!({"status":"failed","success":false}));
242        assert!(!result.is_error());
243        assert_eq!(
244            result.with_text("state: failed".into()).status(),
245            FacadeStatus::Success
246        );
247    }
248}