use super::*;
use crate::output::{availability_label, dns_delegation_verdict, lookup_source};
impl MarkdownFormatter {
pub(super) fn format_lookup(&self, result: &LookupResult) -> String {
let domain = result
.domain_name()
.unwrap_or_else(|| "Unknown".to_string());
let mut output = vec![format!("## Lookup: {}", MdSafe(&domain)), String::new()];
let mut b = Bullets(&mut output);
b.raw("Source", lookup_source(result));
match result {
LookupResult::Rdap {
data,
whois_fallback,
} => {
b.opt("Registrar", &data.get_registrar());
b.opt("Registrant", &data.get_registrant());
b.opt("Organization", &data.get_registrant_organization());
b.date("Created", data.creation_date());
b.expires(data.expiration_date());
b.code_list("Status", &data.status);
b.code_list("Nameservers", &data.nameserver_names());
if data.is_dnssec_signed() {
b.raw("DNSSEC", "signed");
}
let infos = contact::rdap_contacts(data);
let rdap_contacts = contact::rdap_detail_views(&infos);
b.contacts(rdap_contacts);
if let Some(whois) = whois_fallback {
let mut extra = Vec::new();
let mut fill = Bullets(&mut extra);
if data.get_registrant().is_none() {
fill.opt("Registrant", &whois.registrant);
}
if data.get_registrant_organization().is_none() {
fill.opt("Organization", &whois.organization);
}
fill.date("Updated", whois.updated_date);
if !data.is_dnssec_signed() {
fill.opt("DNSSEC", &whois.dnssec);
}
if !whois.whois_server.is_empty() {
fill.code("WHOIS Server", &whois.whois_server);
}
let fallback = contact::ROLES
.into_iter()
.zip(rdap_contacts)
.zip(whois.contacts());
for ((role, rdap), whois_contact) in fallback {
if rdap.is_empty() {
fill.contact(role, whois_contact);
}
}
if !extra.is_empty() {
output.extend([
String::new(),
"### Additional WHOIS Data".to_string(),
String::new(),
]);
output.extend(extra);
}
}
}
LookupResult::Whois {
data,
rdap_error,
dns_nameservers,
..
} => {
b.opt("RDAP Error", rdap_error);
b.opt("Registrar", &data.registrar);
b.opt("Registrant", &data.registrant);
b.opt("Organization", &data.organization);
b.date("Created", data.creation_date);
b.expires(data.expiration_date);
b.code_list("Status", &data.status);
b.code_list("Nameservers", &data.nameservers);
if let Some(dns) = dns_nameservers {
if !dns.in_sync {
b.code_list("Nameservers (DNS)", &dns.delegated);
}
b.text("DNS Delegation", &dns_delegation_verdict(dns));
}
b.opt("DNSSEC", &data.dnssec);
b.contacts(data.contacts());
}
LookupResult::Available {
data,
rdap_error,
whois_error,
whois_data,
} => {
b.raw("Verdict", availability_label(data.verdict()).0);
b.text("Confidence", &data.confidence);
b.text("Method", &data.method);
b.opt("Details", &data.details);
if !rdap_error.is_empty() {
b.text("RDAP Error", rdap_error);
}
if !whois_error.is_empty() {
b.text("WHOIS Error", whois_error);
}
if let Some(w) = whois_data {
let mut extra = Vec::new();
let mut fill = Bullets(&mut extra);
fill.code_list("Nameservers", &w.nameservers);
fill.code_list("Status", &w.status);
fill.opt("DNSSEC", &w.dnssec);
if !w.whois_server.is_empty() {
fill.code("WHOIS Server", &w.whois_server);
}
if !extra.is_empty() {
output.extend([
String::new(),
"### Additional WHOIS Data".to_string(),
String::new(),
]);
output.extend(extra);
}
}
}
}
output.join("\n")
}
pub(super) fn format_availability(
&self,
result: &crate::availability::AvailabilityResult,
) -> String {
let mut output = vec![
format!("## Availability: {}", MdSafe(&result.domain)),
String::new(),
];
let mut b = Bullets(&mut output);
let verdict = if result.available {
"**AVAILABLE**"
} else {
"**TAKEN**"
};
b.raw("Result", verdict);
b.text("Confidence", &result.confidence);
b.text("Method", &result.method);
b.opt("Details", &result.details);
output.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_markdown_format_availability() {
let result = crate::availability::AvailabilityResult {
domain: "test.com".to_string(),
available: true,
confidence: "high".to_string(),
method: "RDAP+WHOIS".to_string(),
details: Some("Domain not found".to_string()),
};
let formatter = MarkdownFormatter::new();
let output = formatter.format_availability(&result);
assert!(output.contains("## Availability: test.com"));
assert!(output.contains("**AVAILABLE**"));
assert!(output.contains("high"));
}
#[test]
fn format_lookup_registered_high_confidence_does_not_say_available() {
let result = LookupResult::Available {
data: Box::new(crate::availability::AvailabilityResult {
domain: "zac.email".to_string(),
available: false,
confidence: "high".to_string(),
method: "dns_present".to_string(),
details: Some("Domain is registered (delegated in DNS).".to_string()),
}),
rdap_error: String::new(),
whois_error: String::new(),
whois_data: None,
};
let out = MarkdownFormatter::new().format_lookup(&result);
assert!(
out.contains("REGISTERED"),
"must render REGISTERED:\n{}",
out
);
assert!(
!out.contains("AVAILABLE"),
"must not render AVAILABLE for a registered domain:\n{}",
out
);
}
#[test]
fn format_lookup_whois_fallback_keeps_organization_only_contacts() {
let mut whois = WhoisResponse::parse("example.com", "whois.test", "Registrar: R\n");
whois.admin_organization = Some("Admin Org LLC".to_string());
whois.tech_organization = Some("Tech Org LLC".to_string());
let rdap: RdapResponse =
serde_json::from_value(serde_json::json!({"ldhName": "example.com"})).unwrap();
let result = LookupResult::Rdap {
data: Box::new(rdap),
whois_fallback: Some(whois),
};
let out = MarkdownFormatter::new().format_lookup(&result);
for needle in [
"### Admin Contact",
"- **Organization**: Admin Org LLC",
"### Tech Contact",
"- **Organization**: Tech Org LLC",
] {
assert!(out.contains(needle), "missing {needle:?}:\n{out}");
}
}
fn assert_fields_before(out: &str, heading: &str, fields: &[&str]) {
let heading_at = out
.find(heading)
.unwrap_or_else(|| panic!("{heading:?} missing:\n{out}"));
for field in fields {
let at = out
.find(field)
.unwrap_or_else(|| panic!("{field:?} missing:\n{out}"));
assert!(
at < heading_at,
"{field:?} rendered under {heading:?}:\n{out}"
);
}
}
fn whois_with_contacts() -> WhoisResponse {
WhoisResponse::parse(
"example.com",
"whois.test",
"Registrar: Mock Registrar\n\
Creation Date: 2010-03-15T04:00:00Z\n\
Updated Date: 2024-02-01T09:30:00Z\n\
Registry Expiry Date: 2099-03-15T04:00:00Z\n\
Registrant Country: US\n\
Admin Name: Jane Admin\n\
Name Server: ns1.example.com\n\
Domain Status: ok\n\
DNSSEC: unsigned\n",
)
}
#[test]
fn format_lookup_whois_domain_fields_precede_contact_sections() {
let result = LookupResult::Whois {
data: whois_with_contacts(),
rdap_error: None,
rdap_fallback: None,
dns_nameservers: None,
};
let out = MarkdownFormatter::new().format_lookup(&result);
assert_fields_before(
&out,
"\n###",
&[
"- **Created**",
"- **Expires**",
"- **Status**",
"- **Nameservers**",
"- **DNSSEC**",
],
);
assert!(out.contains("### Registrant Contact"), "got:\n{out}");
assert!(out.contains("### Admin Contact"), "got:\n{out}");
}
#[test]
fn format_lookup_rdap_fallback_bullets_precede_contact_sections() {
let rdap: RdapResponse = serde_json::from_value(serde_json::json!({
"ldhName": "example.com",
"status": ["active"],
"events": [
{"eventAction": "registration", "eventDate": "2010-03-15T04:00:00Z"}
],
"entities": [{
"objectClassName": "entity",
"handle": "TECH-1",
"roles": ["technical"],
"vcardArray": ["vcard", [["email", {}, "text", "tech@example.com"]]]
}]
}))
.unwrap();
let result = LookupResult::Rdap {
data: Box::new(rdap),
whois_fallback: Some(whois_with_contacts()),
};
let out = MarkdownFormatter::new().format_lookup(&result);
assert_fields_before(&out, "### Tech Contact", &["- **Created**", "- **Status**"]);
assert_fields_before(
&out,
"### Registrant Contact",
&[
"### Additional WHOIS Data",
"- **Updated**",
"- **WHOIS Server**",
],
);
assert!(out.contains("### Admin Contact"), "fallback admin:\n{out}");
}
}