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
47pub struct YamlFormatter;
49
50impl YamlFormatter {
51 pub fn new() -> Self {
52 Self
53 }
54
55 pub fn to_yaml_value<T: serde::Serialize + ?Sized>(&self, value: &T) -> String {
57 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
104fn 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 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}