Skip to main content

seer_core/output/
mod.rs

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