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
59pub struct YamlFormatter;
61
62impl YamlFormatter {
63 pub fn new() -> Self {
64 Self
65 }
66
67 pub fn to_yaml_value<T: Serialize + ?Sized>(&self, value: &T) -> String {
69 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
137fn 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 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}