grapheme_stdlib/
envelope.rs1use serde_json::{json, Value as JsonValue};
7
8pub const ENVELOPE_SCHEMA: &str = "grapheme.host.result.envelope/v1";
9
10pub fn normalize(raw: JsonValue) -> JsonValue {
12 if is_envelope(&raw) {
13 return raw;
14 }
15
16 let error = raw
17 .get("error")
18 .and_then(|v| v.as_str())
19 .map(ToOwned::to_owned);
20
21 if error.is_some() {
22 return json!({
23 "data": raw.get("data").cloned().unwrap_or(JsonValue::Null),
24 "meta": merge_meta(raw.get("meta"), json!({ "legacy_flat": true })),
25 "error": error,
26 });
27 }
28
29 json!({
30 "data": raw,
31 "meta": json!({ "legacy_flat": true }),
32 "error": null,
33 })
34}
35
36pub fn success(data: JsonValue) -> JsonValue {
38 json!({
39 "data": data,
40 "meta": json!({ "schema": ENVELOPE_SCHEMA }),
41 "error": null,
42 })
43}
44
45pub fn failure(error: impl Into<String>) -> JsonValue {
47 json!({
48 "data": null,
49 "meta": json!({ "schema": ENVELOPE_SCHEMA }),
50 "error": error.into(),
51 })
52}
53
54pub fn data<'a>(value: &'a JsonValue) -> &'a JsonValue {
56 if is_envelope(value) {
57 return value.get("data").unwrap_or(value);
58 }
59 value
60}
61
62pub fn is_envelope(value: &JsonValue) -> bool {
63 value.as_object().is_some_and(|obj| {
64 obj.contains_key("data") && obj.contains_key("meta") && obj.contains_key("error")
65 })
66}
67
68fn merge_meta(existing: Option<&JsonValue>, extra: JsonValue) -> JsonValue {
69 let Some(JsonValue::Object(map)) = existing else {
70 return extra;
71 };
72
73 let Some(extra_map) = extra.as_object() else {
74 return JsonValue::Object(map.clone());
75 };
76
77 let mut merged = map.clone();
78 for (key, value) in extra_map.clone() {
79 merged.entry(key).or_insert(value);
80 }
81 JsonValue::Object(merged)
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use serde_json::json;
88
89 #[test]
90 fn wraps_legacy_flat_object() {
91 let out = normalize(json!({ "accepted": true, "count": 3 }));
92 assert_eq!(out.get("data").and_then(|v| v.get("count")).and_then(|v| v.as_u64()), Some(3));
93 assert!(out.get("error").and_then(|v| v.as_null()).is_some());
94 }
95
96 #[test]
97 fn preserves_existing_envelope() {
98 let input = json!({ "data": { "ok": true }, "meta": {}, "error": null });
99 assert_eq!(normalize(input.clone()), input);
100 }
101}