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