conversation_api/execution/
facade_response.rs1use 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#[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 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}