1use 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#[derive(Debug, Clone, PartialEq)]
21pub enum Envelope<T> {
22 Success {
24 version: String,
26 data: T,
28 },
29 Failure {
31 version: String,
33 error: OpError,
35 },
36}
37
38impl<T> Envelope<T> {
39 #[must_use]
41 pub fn success(data: T) -> Self {
42 Self::Success {
43 version: ENVELOPE_VERSION.to_owned(),
44 data,
45 }
46 }
47
48 #[must_use]
50 pub fn failure(error: OpError) -> Self {
51 Self::Failure {
52 version: ENVELOPE_VERSION.to_owned(),
53 error,
54 }
55 }
56
57 #[must_use]
59 pub fn version(&self) -> &str {
60 match self {
61 Self::Success { version, .. } | Self::Failure { version, .. } => version,
62 }
63 }
64
65 #[must_use]
67 pub fn is_ok(&self) -> bool {
68 matches!(self, Self::Success { .. })
69 }
70
71 #[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 #[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 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}
218
219#[cfg(feature = "schema")]
220fn envelope_schema(data: Value, error: Value) -> serde_json::Map<String, Value> {
221 serde_json::Map::from_iter([(
222 "oneOf".into(),
223 serde_json::json!([
224 {"type":"object", "additionalProperties":false,
225 "required":["version","ok","data","error"],
226 "properties":{"version":{"type":"string"},"ok":{"type":"boolean","enum":[true]},"data":data,"error":{"type":"null"}}},
227 {"type":"object", "additionalProperties":false,
228 "required":["version","ok","data","error"],
229 "properties":{"version":{"type":"string"},"ok":{"type":"boolean","enum":[false]},"data":{"type":"null"},"error":error}}
230 ]),
231 )])
232}
233
234#[cfg(feature = "schema")]
235impl<T: schemars::JsonSchema> schemars::JsonSchema for Envelope<T> {
236 fn schema_name() -> std::borrow::Cow<'static, str> {
237 format!("Envelope_{}", T::schema_name()).into()
238 }
239
240 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
241 let value = envelope_schema(
242 generator.subschema_for::<T>().to_value(),
243 generator.subschema_for::<OpError>().to_value(),
244 );
245 schemars::Schema::from(value)
246 }
247}
248
249#[cfg(feature = "schema")]
250impl<T: utoipa::ToSchema> utoipa::__dev::ComposeSchema for Envelope<T> {
251 fn compose(
252 _: Vec<utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>>,
253 ) -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
254 use utoipa::openapi::schema::{AdditionalProperties, ObjectBuilder, OneOfBuilder, Type};
255 let object = || {
256 ObjectBuilder::new()
257 .schema_type(Type::Object)
258 .additional_properties(Some(AdditionalProperties::FreeForm(false)))
259 .required("version")
260 .required("ok")
261 .required("data")
262 .required("error")
263 .property("version", <String as utoipa::PartialSchema>::schema())
264 };
265 let success = object()
266 .property(
267 "ok",
268 ObjectBuilder::new()
269 .schema_type(Type::Boolean)
270 .enum_values(Some([true])),
271 )
272 .property("data", T::schema())
273 .property("error", ObjectBuilder::new().schema_type(Type::Null));
274 let failure = object()
275 .property(
276 "ok",
277 ObjectBuilder::new()
278 .schema_type(Type::Boolean)
279 .enum_values(Some([false])),
280 )
281 .property("data", ObjectBuilder::new().schema_type(Type::Null))
282 .property("error", <OpError as utoipa::PartialSchema>::schema());
283 OneOfBuilder::new().item(success).item(failure).into()
284 }
285}
286
287#[cfg(feature = "schema")]
288impl<T: utoipa::ToSchema> utoipa::ToSchema for Envelope<T> {
289 fn name() -> std::borrow::Cow<'static, str> {
290 format!("Envelope_{}", T::name()).into()
291 }
292
293 fn schemas(
294 schemas: &mut Vec<(
295 String,
296 utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
297 )>,
298 ) {
299 schemas.push((T::name().into_owned(), T::schema()));
300 T::schemas(schemas);
301 }
302}
303
304#[cfg(all(test, feature = "schema"))]
305mod typed_projection_schema {
306 use super::Envelope;
307 use utoipa::OpenApi;
308
309 #[derive(utoipa::ToSchema)]
310 struct Body {
311 value: String,
312 }
313
314 #[utoipa::path(post, path = "/typed", responses((status = 200, body = Envelope<Body>)))]
315 fn typed_route() {}
316
317 #[derive(utoipa::OpenApi)]
318 #[openapi(paths(typed_route))]
319 struct Api;
320
321 #[test]
322 fn actual_typed_path_preserves_generic_envelope_payload_schema() {
323 typed_route();
324 let body = Body {
325 value: "typed".into(),
326 };
327 assert_eq!(body.value, "typed");
328 let specification = serde_json::to_value(Api::openapi()).expect("OpenAPI");
329 let response = &specification["paths"]["/typed"]["post"]["responses"]["200"];
330 assert!(response.to_string().contains("Envelope_Body"));
331 let reference = response["content"]["application/json"]["schema"]["$ref"]
332 .as_str()
333 .expect("typed response schema reference");
334 let component = reference
335 .strip_prefix("#/components/schemas/")
336 .expect("component reference");
337 let schema = &specification["components"]["schemas"][component];
338 assert_eq!(
339 schema["oneOf"][0]["properties"]["data"]["properties"]["value"]["type"],
340 "string"
341 );
342 assert_eq!(schema["oneOf"][1]["properties"]["data"]["type"], "null");
343 }
344}