Skip to main content

seer_core/output/
mod.rs

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