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/// Invocation status is independent of payload presence. Successful acknowledgements
32/// omit data; partial outcomes always retain their result. Full data is a business
33/// value and essential data is YAML text, selected by the request, not inferred here.
34#[derive(Clone, Debug, PartialEq, Serialize)]
35#[serde(tag = "status", rename_all = "snake_case")]
36pub enum FacadeResponse {
37    Success {
38        #[serde(skip_serializing_if = "Option::is_none")]
39        data: Option<Value>,
40        #[serde(skip_serializing_if = "Vec::is_empty")]
41        warnings: Vec<FacadeProblem>,
42    },
43    Partial {
44        data: Value,
45        #[serde(skip_serializing_if = "Vec::is_empty")]
46        warnings: Vec<FacadeProblem>,
47    },
48    Error {
49        error: FacadeProblem,
50        #[serde(skip_serializing_if = "Option::is_none")]
51        data: Option<String>,
52    },
53    Cancelled {
54        #[serde(skip_serializing_if = "Vec::is_empty")]
55        warnings: Vec<FacadeProblem>,
56    },
57}
58
59impl FacadeResponse {
60    /// A completed operation with no business value to return.
61    pub fn acknowledged() -> Self {
62        Self::Success {
63            data: None,
64            warnings: vec![],
65        }
66    }
67    pub fn cancelled() -> Self {
68        Self::Cancelled { warnings: vec![] }
69    }
70    pub fn success(data: Value) -> Self {
71        Self::Success {
72            data: Some(data),
73            warnings: vec![],
74        }
75    }
76    pub fn partial(data: Value) -> Self {
77        Self::Partial {
78            data,
79            warnings: vec![],
80        }
81    }
82    pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
83        Self::Error {
84            error: FacadeProblem {
85                code: code.into(),
86                message: message.into(),
87                details: None,
88            },
89            data: None,
90        }
91    }
92    pub fn with_details(mut self, details: Value) -> Self {
93        if let Self::Error { error, .. } = &mut self {
94            error.details = Some(details);
95        }
96        self
97    }
98    pub fn status(&self) -> FacadeStatus {
99        match self {
100            Self::Success { .. } => FacadeStatus::Success,
101            Self::Partial { .. } => FacadeStatus::Partial,
102            Self::Error { .. } => FacadeStatus::Error,
103            Self::Cancelled { .. } => FacadeStatus::Cancelled,
104        }
105    }
106    pub fn is_error(&self) -> bool {
107        matches!(self, Self::Partial { .. } | Self::Error { .. })
108    }
109    pub fn data(&self) -> Option<&Value> {
110        match self {
111            Self::Success { data, .. } => data.as_ref(),
112            Self::Partial { data, .. } => Some(data),
113            _ => None,
114        }
115    }
116    pub fn data_mut(&mut self) -> Option<&mut Value> {
117        match self {
118            Self::Success { data, .. } => data.as_mut(),
119            Self::Partial { data, .. } => Some(data),
120            _ => None,
121        }
122    }
123    /// Set the Essential presentation once, preserving the execution outcome.
124    /// Full error details move into the projected diagnostic data. Acknowledgements
125    /// and cancellations remain payload-free.
126    pub fn with_essential_data(self, text: String) -> Self {
127        match self {
128            Self::Success {
129                data: Some(_),
130                warnings,
131            } => Self::Success {
132                data: Some(Value::String(text)),
133                warnings,
134            },
135            Self::Partial { warnings, .. } => Self::Partial {
136                data: Value::String(text),
137                warnings,
138            },
139            Self::Error { mut error, .. } => {
140                error.details = None;
141                Self::Error {
142                    error,
143                    data: Some(text),
144                }
145            }
146            terminal => terminal,
147        }
148    }
149    pub fn with_warning(mut self, warning: FacadeProblem) -> Self {
150        match &mut self {
151            Self::Success { warnings, .. }
152            | Self::Partial { warnings, .. }
153            | Self::Cancelled { warnings } => warnings.push(warning),
154            _ => {}
155        }
156        self
157    }
158}
159
160impl<'de> Deserialize<'de> for FacadeResponse {
161    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
162        use serde::de::Error;
163        let value = Value::deserialize(deserializer)?;
164        let mut fields = value
165            .as_object()
166            .cloned()
167            .ok_or_else(|| D::Error::custom("Facade response must be an object"))?;
168        let status = fields
169            .remove("status")
170            .ok_or_else(|| D::Error::custom("Missing Facade status"))?;
171        let response = match status.as_str() {
172            Some("success" | "partial") => {
173                let data = fields.remove("data");
174                if status == "partial" && data.is_none() {
175                    return Err(D::Error::custom("Partial outcome requires data"));
176                }
177                let warnings = fields
178                    .remove("warnings")
179                    .map(serde_json::from_value)
180                    .transpose()
181                    .map_err(D::Error::custom)?
182                    .unwrap_or_default();
183                if status == "success" {
184                    Self::Success { data, warnings }
185                } else {
186                    Self::Partial {
187                        data: data.expect("partial data checked"),
188                        warnings,
189                    }
190                }
191            }
192            Some("error") => Self::Error {
193                data: fields
194                    .remove("data")
195                    .map(serde_json::from_value)
196                    .transpose()
197                    .map_err(D::Error::custom)?,
198                error: serde_json::from_value(
199                    fields
200                        .remove("error")
201                        .ok_or_else(|| D::Error::custom("Missing Facade error"))?,
202                )
203                .map_err(D::Error::custom)?,
204            },
205            Some("cancelled") => Self::Cancelled {
206                warnings: fields
207                    .remove("warnings")
208                    .map(serde_json::from_value)
209                    .transpose()
210                    .map_err(D::Error::custom)?
211                    .unwrap_or_default(),
212            },
213            _ => return Err(D::Error::custom("Unknown Facade status")),
214        };
215        if !fields.is_empty() {
216            return Err(D::Error::custom("Unknown Facade response field"));
217        }
218        Ok(response)
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use serde_json::json;
226    #[test]
227    fn strict_response_roundtrip() {
228        for value in [
229            json!({"status":"success","data":null}),
230            json!({"status":"success","data":"items: []"}),
231            json!({"status":"success"}),
232            json!({"status":"success","warnings":[{"code":"SYNC_PENDING","message":"Saved"}]}),
233            json!({"status":"partial","data":{"done":["a"],"failed":["b"]}}),
234            json!({"status":"error","error":{"code":"STALE","message":"Changed"}}),
235            json!({"status":"error","error":{"code":"BAD_REQUEST","message":"Invalid"},"data":"status: error\n"}),
236            json!({"status":"cancelled"}),
237        ] {
238            let parsed: FacadeResponse = serde_json::from_value(value.clone()).unwrap();
239            assert_eq!(serde_json::to_value(parsed).unwrap(), value);
240        }
241        for value in [
242            json!({"status":"partial"}),
243            json!({"status":"success","text":"legacy"}),
244            json!({"status":"success","data":{},"text":"bad"}),
245            json!({"status":"cancelled","data":{}}),
246            json!({"status":"error","error":{"message":"bad"}}),
247            json!({"status":"error","error":{"code":"BAD","message":"Invalid"},"data":{}}),
248            json!({"status":"error","error":{"code":"BAD","message":"Invalid"},"text":"legacy"}),
249            json!({"status":"done","data":{}}),
250        ] {
251            assert!(serde_json::from_value::<FacadeResponse>(value).is_err());
252        }
253    }
254    #[test]
255    fn resource_failure_is_not_invocation_failure() {
256        let result = FacadeResponse::success(json!({"status":"failed","success":false}));
257        assert!(!result.is_error());
258        assert_eq!(
259            result.with_essential_data("state: failed".into()).status(),
260            FacadeStatus::Success
261        );
262    }
263}