use std::collections::HashSet;
use std::sync::Arc;
use crate::domain::Tld;
use crate::registry::{BootstrapRegistry, Endpoint, Registry, RegistryProvider};
pub const DEFAULT_TEMPLATE: &str = "whois.nic.{tld}";
#[derive(Debug, Clone)]
pub struct ConventionRegistry {
known: HashSet<Tld>,
template: String,
}
impl ConventionRegistry {
pub fn new(known: impl IntoIterator<Item = Tld>) -> Self {
ConventionRegistry {
known: known.into_iter().collect(),
template: DEFAULT_TEMPLATE.to_string(),
}
}
pub fn from_iana() -> Self {
ConventionRegistry::new(BootstrapRegistry::bundled().tlds())
}
pub fn with_template(mut self, template: impl Into<String>) -> Self {
self.template = template.into();
self
}
pub fn template(&self) -> &str {
&self.template
}
pub fn len(&self) -> usize {
self.known.len()
}
pub fn is_empty(&self) -> bool {
self.known.is_empty()
}
fn host_for(&self, tld: &Tld) -> String {
self.template.replace("{tld}", tld.root_label())
}
}
impl RegistryProvider for ConventionRegistry {
fn get(&self, tld: &Tld) -> Option<Arc<Registry>> {
if !self.known.contains(tld) {
return None;
}
Some(
Registry::builder([tld.clone()])
.endpoint(Endpoint::whois(self.host_for(tld)))
.note(format!(
"host guessed from the {} convention",
self.template
))
.build_shared(),
)
}
fn tlds(&self) -> Vec<Tld> {
let mut tlds: Vec<Tld> = self.known.iter().cloned().collect();
tlds.sort();
tlds
}
fn describe(&self) -> String {
format!("convention {} ({} tlds)", self.template, self.known.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derives_the_conventional_host() {
let registry = ConventionRegistry::new([Tld::parse("example").unwrap()]);
let entry = registry.get(&Tld::parse("example").unwrap()).unwrap();
assert_eq!(entry.endpoints()[0].address(), "whois.nic.example");
assert!(entry.note().unwrap().contains("guessed"));
}
#[test]
fn answers_nothing_outside_the_allow_list() {
let registry = ConventionRegistry::new([Tld::parse("example").unwrap()]);
assert!(registry.get(&Tld::parse("other").unwrap()).is_none());
assert!(ConventionRegistry::new([])
.get(&Tld::parse("com").unwrap())
.is_none());
}
#[test]
fn multi_label_suffixes_use_the_root_label() {
let registry = ConventionRegistry::new([Tld::parse("co.uk").unwrap()]);
let entry = registry.get(&Tld::parse("co.uk").unwrap()).unwrap();
assert_eq!(entry.endpoints()[0].address(), "whois.nic.uk");
}
#[test]
fn honours_a_custom_template() {
let registry = ConventionRegistry::new([Tld::parse("example").unwrap()])
.with_template("whois.{tld}.test");
let entry = registry.get(&Tld::parse("example").unwrap()).unwrap();
assert_eq!(entry.endpoints()[0].address(), "whois.example.test");
}
#[test]
fn the_iana_allow_list_is_the_full_tld_set() {
let registry = ConventionRegistry::from_iana();
assert!(registry.len() > 1000, "got {}", registry.len());
assert!(registry.get(&Tld::parse("com").unwrap()).is_some());
assert!(registry.get(&Tld::parse("example.com").unwrap()).is_none());
}
}