Skip to main content

seer_core/output/
mod.rs

1mod human;
2mod json;
3
4pub use human::HumanFormatter;
5pub use json::JsonFormatter;
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum OutputFormat {
12    #[default]
13    Human,
14    Json,
15    Yaml,
16}
17
18impl std::str::FromStr for OutputFormat {
19    type Err = String;
20
21    fn from_str(s: &str) -> Result<Self, Self::Err> {
22        match s.to_lowercase().as_str() {
23            "human" | "text" | "pretty" => Ok(OutputFormat::Human),
24            "json" => Ok(OutputFormat::Json),
25            "yaml" | "yml" => Ok(OutputFormat::Yaml),
26            _ => Err(format!(
27                "Unknown output format: {}. Use: human, json, yaml",
28                s
29            )),
30        }
31    }
32}
33
34pub trait OutputFormatter {
35    fn format_whois(&self, response: &crate::whois::WhoisResponse) -> String;
36    fn format_rdap(&self, response: &crate::rdap::RdapResponse) -> String;
37    fn format_dns(&self, records: &[crate::dns::DnsRecord]) -> String;
38    fn format_propagation(&self, result: &crate::dns::PropagationResult) -> String;
39    fn format_lookup(&self, result: &crate::lookup::LookupResult) -> String;
40    fn format_status(&self, response: &crate::status::StatusResponse) -> String;
41    fn format_follow_iteration(&self, iteration: &crate::dns::FollowIteration) -> String;
42    fn format_follow(&self, result: &crate::dns::FollowResult) -> String;
43    fn format_availability(&self, result: &crate::availability::AvailabilityResult) -> String;
44    fn format_dnssec(&self, report: &crate::dns::DnssecReport) -> String;
45}
46
47/// YAML output formatter that converts data structures to YAML format.
48pub struct YamlFormatter;
49
50impl YamlFormatter {
51    pub fn new() -> Self {
52        Self
53    }
54
55    /// Formats any serializable value as YAML output.
56    pub fn to_yaml_value<T: serde::Serialize + ?Sized>(&self, value: &T) -> String {
57        // Convert to JSON value first, then format as YAML-like output
58        match serde_json::to_value(value) {
59            Ok(v) => format_as_yaml(&v, 0),
60            Err(e) => format!("error: {}", e),
61        }
62    }
63}
64
65impl Default for YamlFormatter {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71impl OutputFormatter for YamlFormatter {
72    fn format_whois(&self, response: &crate::whois::WhoisResponse) -> String {
73        self.to_yaml_value(response)
74    }
75    fn format_rdap(&self, response: &crate::rdap::RdapResponse) -> String {
76        self.to_yaml_value(response)
77    }
78    fn format_dns(&self, records: &[crate::dns::DnsRecord]) -> String {
79        self.to_yaml_value(records)
80    }
81    fn format_propagation(&self, result: &crate::dns::PropagationResult) -> String {
82        self.to_yaml_value(result)
83    }
84    fn format_lookup(&self, result: &crate::lookup::LookupResult) -> String {
85        self.to_yaml_value(result)
86    }
87    fn format_status(&self, response: &crate::status::StatusResponse) -> String {
88        self.to_yaml_value(response)
89    }
90    fn format_follow_iteration(&self, iteration: &crate::dns::FollowIteration) -> String {
91        self.to_yaml_value(iteration)
92    }
93    fn format_follow(&self, result: &crate::dns::FollowResult) -> String {
94        self.to_yaml_value(result)
95    }
96    fn format_availability(&self, result: &crate::availability::AvailabilityResult) -> String {
97        self.to_yaml_value(result)
98    }
99    fn format_dnssec(&self, report: &crate::dns::DnssecReport) -> String {
100        self.to_yaml_value(report)
101    }
102}
103
104/// Simple YAML-like formatter from serde_json::Value.
105fn format_as_yaml(value: &serde_json::Value, indent: usize) -> String {
106    let prefix = "  ".repeat(indent);
107    match value {
108        serde_json::Value::Null => "null".to_string(),
109        serde_json::Value::Bool(b) => b.to_string(),
110        serde_json::Value::Number(n) => n.to_string(),
111        serde_json::Value::String(s) => {
112            if s.contains('\n') || s.contains(':') || s.contains('#') {
113                format!("\"{}\"", s.replace('"', "\\\""))
114            } else {
115                s.clone()
116            }
117        }
118        serde_json::Value::Array(arr) => {
119            if arr.is_empty() {
120                return "[]".to_string();
121            }
122            let mut out = String::new();
123            for item in arr {
124                out.push('\n');
125                out.push_str(&prefix);
126                out.push_str("- ");
127                let formatted = format_as_yaml(item, indent + 1);
128                out.push_str(&formatted);
129            }
130            out
131        }
132        serde_json::Value::Object(map) => {
133            if map.is_empty() {
134                return "{}".to_string();
135            }
136            let mut out = String::new();
137            let mut first = indent == 0;
138            for (key, val) in map {
139                if !first {
140                    out.push('\n');
141                }
142                first = false;
143                out.push_str(&prefix);
144                out.push_str(key);
145                out.push_str(": ");
146                match val {
147                    serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
148                        out.push_str(&format_as_yaml(val, indent + 1));
149                    }
150                    _ => {
151                        out.push_str(&format_as_yaml(val, indent));
152                    }
153                }
154            }
155            out
156        }
157    }
158}
159
160pub fn get_formatter(format: OutputFormat) -> Box<dyn OutputFormatter> {
161    match format {
162        OutputFormat::Human => Box::new(HumanFormatter::new()),
163        OutputFormat::Json => Box::new(JsonFormatter::new()),
164        OutputFormat::Yaml => Box::new(YamlFormatter::new()),
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_output_format_from_str() {
174        assert_eq!(
175            "human".parse::<OutputFormat>().unwrap(),
176            OutputFormat::Human
177        );
178        assert_eq!("json".parse::<OutputFormat>().unwrap(), OutputFormat::Json);
179        assert_eq!("yaml".parse::<OutputFormat>().unwrap(), OutputFormat::Yaml);
180        assert_eq!("yml".parse::<OutputFormat>().unwrap(), OutputFormat::Yaml);
181        assert_eq!("text".parse::<OutputFormat>().unwrap(), OutputFormat::Human);
182        assert_eq!(
183            "pretty".parse::<OutputFormat>().unwrap(),
184            OutputFormat::Human
185        );
186        assert!("invalid".parse::<OutputFormat>().is_err());
187    }
188
189    #[test]
190    fn test_output_format_default() {
191        assert_eq!(OutputFormat::default(), OutputFormat::Human);
192    }
193
194    #[test]
195    fn test_get_formatter_returns_correct_type() {
196        // Just verify we can get formatters without panicking
197        let _ = get_formatter(OutputFormat::Human);
198        let _ = get_formatter(OutputFormat::Json);
199        let _ = get_formatter(OutputFormat::Yaml);
200    }
201
202    #[test]
203    fn test_yaml_formatter_basic() {
204        let formatter = YamlFormatter::new();
205        let status = crate::status::StatusResponse::new("example.com".to_string());
206        let output = formatter.format_status(&status);
207        assert!(output.contains("example.com"));
208        assert!(output.contains("domain"));
209    }
210
211    #[test]
212    fn test_format_as_yaml_primitives() {
213        assert_eq!(format_as_yaml(&serde_json::json!(null), 0), "null");
214        assert_eq!(format_as_yaml(&serde_json::json!(true), 0), "true");
215        assert_eq!(format_as_yaml(&serde_json::json!(42), 0), "42");
216        assert_eq!(format_as_yaml(&serde_json::json!("hello"), 0), "hello");
217    }
218
219    #[test]
220    fn test_format_as_yaml_array() {
221        let output = format_as_yaml(&serde_json::json!([1, 2, 3]), 0);
222        assert!(output.contains("- 1"));
223        assert!(output.contains("- 2"));
224        assert!(output.contains("- 3"));
225    }
226
227    #[test]
228    fn test_format_as_yaml_empty_collections() {
229        assert_eq!(format_as_yaml(&serde_json::json!([]), 0), "[]");
230        assert_eq!(format_as_yaml(&serde_json::json!({}), 0), "{}");
231    }
232}