1use anyhow::Result;
8use serde::Serialize;
9use serde_json::Value;
10
11#[derive(Debug, Clone, Serialize)]
12pub struct ErrorEnvelope {
13 pub code: String,
14 pub message: String,
15 #[serde(skip_serializing_if = "Option::is_none")]
16 pub details: Option<Value>,
17}
18
19#[derive(Debug, Clone, Serialize)]
20pub struct JsonEnvelopeResult<T: Serialize> {
21 pub schema_version: String,
22 pub command: String,
23 pub ok: bool,
24 pub result: T,
25}
26
27#[derive(Debug, Clone, Serialize)]
28pub struct JsonEnvelopeResults<T: Serialize> {
29 pub schema_version: String,
30 pub command: String,
31 pub ok: bool,
32 pub results: Vec<T>,
33}
34
35#[derive(Debug, Clone, Serialize)]
36pub struct JsonEnvelopeError {
37 pub schema_version: String,
38 pub command: String,
39 pub ok: bool,
40 pub error: ErrorEnvelope,
41}
42
43pub fn emit_json<T: Serialize>(payload: &T) -> Result<()> {
44 println!("{}", serde_json::to_string(payload)?);
45 Ok(())
46}
47
48pub fn emit_success_result<T: Serialize>(
49 schema_version: &str,
50 command: &str,
51 result: T,
52) -> Result<()> {
53 emit_json(&JsonEnvelopeResult {
54 schema_version: schema_version.to_string(),
55 command: command.to_string(),
56 ok: true,
57 result,
58 })
59}
60
61pub fn emit_success_results<T: Serialize>(
62 schema_version: &str,
63 command: &str,
64 results: Vec<T>,
65) -> Result<()> {
66 emit_json(&JsonEnvelopeResults {
67 schema_version: schema_version.to_string(),
68 command: command.to_string(),
69 ok: true,
70 results,
71 })
72}
73
74pub fn emit_error(
75 schema_version: &str,
76 command: &str,
77 code: &str,
78 message: impl Into<String>,
79 details: Option<Value>,
80) -> Result<()> {
81 emit_json(&JsonEnvelopeError {
82 schema_version: schema_version.to_string(),
83 command: command.to_string(),
84 ok: false,
85 error: ErrorEnvelope {
86 code: code.to_string(),
87 message: message.into(),
88 details,
89 },
90 })
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use serde_json::{json, to_value};
97
98 const TEST_SCHEMA: &str = "nils-common.diag-output.test.v1";
99
100 #[test]
101 fn error_envelope_serialization_omits_details_when_none() {
102 let envelope = JsonEnvelopeError {
103 schema_version: TEST_SCHEMA.to_string(),
104 command: "diag test".to_string(),
105 ok: false,
106 error: ErrorEnvelope {
107 code: "bad-input".to_string(),
108 message: "invalid".to_string(),
109 details: None,
110 },
111 };
112 let value = to_value(envelope).expect("serialize");
113 assert_eq!(value["ok"], false);
114 assert!(value["error"].get("details").is_none());
115 }
116
117 #[test]
118 fn error_envelope_serialization_emits_details_when_present() {
119 let envelope = JsonEnvelopeError {
120 schema_version: TEST_SCHEMA.to_string(),
121 command: "diag test".to_string(),
122 ok: false,
123 error: ErrorEnvelope {
124 code: "bad-input".to_string(),
125 message: "invalid".to_string(),
126 details: Some(json!({"hint": "retry"})),
127 },
128 };
129 let value = to_value(envelope).expect("serialize");
130 assert_eq!(value["error"]["details"], json!({"hint": "retry"}));
131 }
132
133 #[test]
134 fn success_result_serialization_round_trip() {
135 let payload = JsonEnvelopeResult {
136 schema_version: TEST_SCHEMA.to_string(),
137 command: "diag list".to_string(),
138 ok: true,
139 result: json!({"status": "ok"}),
140 };
141 let value = to_value(&payload).expect("serialize");
142 assert_eq!(value["schema_version"], TEST_SCHEMA);
143 assert_eq!(value["command"], "diag list");
144 assert_eq!(value["ok"], true);
145 assert_eq!(value["result"]["status"], "ok");
146 }
147
148 #[test]
149 fn success_results_serialization_preserves_vec_order() {
150 let payload = JsonEnvelopeResults {
151 schema_version: TEST_SCHEMA.to_string(),
152 command: "diag list".to_string(),
153 ok: true,
154 results: vec![json!({"item": 1}), json!({"item": 2})],
155 };
156 let value = to_value(&payload).expect("serialize");
157 assert_eq!(value["results"][0]["item"], 1);
158 assert_eq!(value["results"][1]["item"], 2);
159 }
160
161 #[test]
162 fn emit_helpers_return_ok() {
163 assert!(emit_success_result(TEST_SCHEMA, "diag test", json!({"status": "ok"})).is_ok());
164 assert!(
165 emit_success_results(
166 TEST_SCHEMA,
167 "diag test",
168 vec![json!({"item": 1}), json!({"item": 2})],
169 )
170 .is_ok()
171 );
172 assert!(
173 emit_error(
174 TEST_SCHEMA,
175 "diag test",
176 "failure",
177 "boom",
178 Some(json!({"hint": "retry"})),
179 )
180 .is_ok()
181 );
182 assert!(emit_error(TEST_SCHEMA, "diag test", "failure", "boom", None).is_ok());
183 }
184}