Skip to main content

seer_core/output/
json.rs

1use super::OutputFormatter;
2
3/// Pretty-printed JSON output formatter.
4#[derive(Default)]
5pub struct JsonFormatter;
6
7impl JsonFormatter {
8    pub fn new() -> Self {
9        Self
10    }
11
12    fn to_json<T: serde::Serialize + ?Sized>(&self, value: &T) -> String {
13        // Built through `json!` so the error text is escaped: a message
14        // carrying a quote must not yield invalid JSON.
15        serde_json::to_string_pretty(value)
16            .unwrap_or_else(|e| serde_json::json!({ "error": e.to_string() }).to_string())
17    }
18}
19
20with_report_methods!(impl_serializing!(JsonFormatter, to_json;));
21
22#[cfg(test)]
23mod tests {
24    use super::*;
25    use crate::dns::{DnsRecord, RecordType};
26    use crate::status::StatusResponse;
27
28    #[test]
29    fn test_json_format_status() {
30        let response = StatusResponse::new("example.com".to_string());
31        let formatter = JsonFormatter::new();
32        let output = formatter.format_status(&response);
33        assert!(output.contains("example.com"));
34        assert!(output.contains("domain"));
35    }
36
37    #[test]
38    fn test_json_format_dns_records() {
39        let records = vec![DnsRecord {
40            name: "example.com".to_string(),
41            record_type: RecordType::A,
42            ttl: 300,
43            data: crate::dns::RecordData::A {
44                address: "93.184.216.34".to_string(),
45            },
46        }];
47        let formatter = JsonFormatter::new();
48        let output = formatter.format_dns(&records);
49        assert!(output.contains("93.184.216.34"));
50        assert!(output.contains("\"A\""));
51    }
52
53    #[test]
54    fn error_fallback_is_valid_json() {
55        // A serializer error carrying a quote was interpolated raw into
56        // `{"error": "…"}`, producing invalid JSON.
57        struct Failing;
58        impl serde::Serialize for Failing {
59            fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
60                Err(serde::ser::Error::custom("bad \"value\"\n"))
61            }
62        }
63        let out = JsonFormatter::new().to_json(&Failing);
64        let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
65        assert_eq!(parsed["error"], "bad \"value\"\n");
66    }
67
68    #[test]
69    fn dig_and_trace_serialize_as_objects() {
70        // `seer dig -q` / `--format json` emit the whole result object (status,
71        // flags, answers, …), not the record list `format_dns` emits.
72        use crate::dns::{DnsQueryResult, DnsStatus, DnsTrace};
73        let result = DnsQueryResult {
74            name: "gone.seer.test".to_string(),
75            record_type: RecordType::A,
76            server: None,
77            answered_locally: false,
78            status: DnsStatus::NxDomain,
79            flags: Vec::new(),
80            answers: Vec::new(),
81            failed_types: Vec::new(),
82            authority: Vec::new(),
83            wildcard: None,
84            query_time_ms: 7,
85        };
86        let json: serde_json::Value =
87            serde_json::from_str(&JsonFormatter::new().format_dig(&result)).unwrap();
88        assert_eq!(json["status"], "NXDOMAIN");
89        assert_eq!(json["server"], serde_json::Value::Null);
90
91        let trace = DnsTrace {
92            name: "gone.seer.test".to_string(),
93            record_type: RecordType::A,
94            hops: Vec::new(),
95            status: DnsStatus::NxDomain,
96            answers: Vec::new(),
97            error: Some("stopped".to_string()),
98        };
99        let json: serde_json::Value =
100            serde_json::from_str(&JsonFormatter::new().format_dns_trace(&trace)).unwrap();
101        assert_eq!(json["status"], "NXDOMAIN");
102        assert_eq!(json["error"], "stopped");
103    }
104}