Skip to main content

tftio_cli_common/
json.rs

1//! Shared JSON response helpers.
2
3use serde_json::{Value, json};
4
5/// Build a success response envelope.
6#[must_use]
7#[allow(clippy::needless_pass_by_value)]
8pub fn ok_response(command: &str, data: Value) -> Value {
9    json!({
10        "ok": true,
11        "command": command,
12        "data": data
13    })
14}
15
16/// Build an error response envelope.
17#[must_use]
18#[allow(clippy::needless_pass_by_value)]
19pub fn err_response(command: &str, code: &str, message: &str, details: Value) -> Value {
20    json!({
21        "ok": false,
22        "command": command,
23        "error": {
24            "code": code,
25            "message": message,
26            "details": details
27        }
28    })
29}
30
31/// Render either the shared JSON envelope or plain text for a command response.
32#[must_use]
33#[allow(clippy::needless_pass_by_value)]
34pub fn render_response(
35    command: &str,
36    json_output: bool,
37    data: Value,
38    text: impl Into<String>,
39) -> String {
40    render_response_parts(command, json_output, || data, || text.into())
41}
42
43/// Render either the shared JSON envelope or lazily-built plain text for a command response.
44#[must_use]
45#[allow(clippy::needless_pass_by_value)]
46pub fn render_response_with<F>(command: &str, json_output: bool, data: Value, text: F) -> String
47where
48    F: FnOnce() -> String,
49{
50    render_response_parts(command, json_output, || data, text)
51}
52
53/// Render either the shared JSON envelope or lazily-built command data and plain text.
54#[must_use]
55#[allow(clippy::needless_pass_by_value)]
56pub fn render_response_parts<D, T>(command: &str, json_output: bool, data: D, text: T) -> String
57where
58    D: FnOnce() -> Value,
59    T: FnOnce() -> String,
60{
61    if json_output {
62        ok_response(command, data()).to_string()
63    } else {
64        text()
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use std::cell::Cell;
71
72    use serde_json::json;
73
74    use super::*;
75
76    #[test]
77    fn ok_response_contains_expected_shape() {
78        let value = ok_response("list", json!({ "x": 1 }));
79        assert_eq!(value["ok"], json!(true));
80        assert_eq!(value["command"], json!("list"));
81        assert_eq!(value["data"]["x"], json!(1));
82    }
83
84    #[test]
85    fn err_response_contains_expected_shape() {
86        let value = err_response("list", "ERROR", "bad", json!({}));
87        assert_eq!(value["ok"], json!(false));
88        assert_eq!(value["error"]["code"], json!("ERROR"));
89        assert_eq!(value["error"]["message"], json!("bad"));
90    }
91
92    #[test]
93    fn render_response_uses_json_envelope_when_requested() {
94        let value = render_response("list", true, json!({"x": 1}), "text");
95        assert!(value.contains("\"ok\":true"));
96    }
97
98    #[test]
99    fn render_response_with_skips_text_builder_for_json_output() {
100        let called = Cell::new(false);
101        let value = render_response_with("list", true, json!({"x": 1}), || {
102            called.set(true);
103            String::from("text")
104        });
105
106        assert!(value.contains("\"ok\":true"));
107        assert!(!called.get());
108    }
109
110    #[test]
111    fn render_response_with_builds_text_for_text_output() {
112        let value = render_response_with("list", false, json!({"x": 1}), || String::from("text"));
113        assert_eq!(value, "text");
114    }
115
116    #[test]
117    fn render_response_parts_skips_text_builder_for_json_output() {
118        let called = Cell::new(false);
119        let value = render_response_parts(
120            "list",
121            true,
122            || json!({"x": 1}),
123            || {
124                called.set(true);
125                String::from("text")
126            },
127        );
128
129        assert!(value.contains("\"ok\":true"));
130        assert!(!called.get());
131    }
132
133    #[test]
134    fn render_response_parts_skips_data_builder_for_text_output() {
135        let called = Cell::new(false);
136        let value = render_response_parts(
137            "list",
138            false,
139            || {
140                called.set(true);
141                json!({"x": 1})
142            },
143            || String::from("text"),
144        );
145
146        assert_eq!(value, "text");
147        assert!(!called.get());
148    }
149}