use std::fmt;
use std::sync::Arc;
use crate::domain::{DomainName, Tld};
use crate::error::{Error, Result};
use crate::registry::Registry;
pub trait RegistryProvider: fmt::Debug + Send + Sync {
fn get(&self, tld: &Tld) -> Option<Arc<Registry>>;
fn tlds(&self) -> Vec<Tld>;
fn describe(&self) -> String {
format!("{self:?}")
}
fn resolve(&self, name: &DomainName) -> Result<Resolution> {
for split in name.suffix_candidates() {
let tld = Tld::from_ascii_unchecked(split.suffix);
if let Some(registry) = self.get(&tld) {
let registrable = name.registrable_under(&tld).unwrap_or_else(|| name.clone());
return Ok(Resolution {
queried: name.clone(),
registrable,
tld,
registry,
});
}
}
let root = name
.suffix_candidates()
.last()
.map(|split| Tld::from_ascii_unchecked(split.suffix))
.unwrap_or_else(|| Tld::from_ascii_unchecked(name.as_ascii()));
Err(Error::UnsupportedTld { tld: root })
}
fn can_resolve(&self, name: &DomainName) -> bool {
self.resolve(name).is_ok()
}
}
#[derive(Debug, Clone)]
pub struct Resolution {
pub queried: DomainName,
pub registrable: DomainName,
pub tld: Tld,
pub registry: Arc<Registry>,
}
impl Resolution {
pub fn sld(&self) -> &str {
self.registrable
.as_ascii()
.strip_suffix(self.tld.ascii())
.map(|head| head.trim_end_matches('.'))
.unwrap_or_else(|| self.registrable.as_ascii())
}
pub fn is_subdomain(&self) -> bool {
self.queried != self.registrable
}
pub fn wire_form(&self) -> String {
self.registry.wire_form(&self.registrable)
}
}
impl<T: RegistryProvider + ?Sized> RegistryProvider for Arc<T> {
fn get(&self, tld: &Tld) -> Option<Arc<Registry>> {
(**self).get(tld)
}
fn tlds(&self) -> Vec<Tld> {
(**self).tlds()
}
fn describe(&self) -> String {
(**self).describe()
}
}
impl<T: RegistryProvider + ?Sized> RegistryProvider for Box<T> {
fn get(&self, tld: &Tld) -> Option<Arc<Registry>> {
(**self).get(tld)
}
fn tlds(&self) -> Vec<Tld> {
(**self).tlds()
}
fn describe(&self) -> String {
(**self).describe()
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
use crate::registry::{Endpoint, Registry};
#[derive(Debug)]
struct Fixed(HashMap<Tld, Arc<Registry>>);
impl Fixed {
fn new(tlds: &[&str]) -> Self {
let map = tlds
.iter()
.map(|raw| {
let tld = Tld::parse(raw).unwrap();
let registry = Registry::builder([tld.clone()])
.endpoint(Endpoint::whois(format!("whois.nic.{raw}")))
.build_shared();
(tld, registry)
})
.collect();
Fixed(map)
}
}
impl RegistryProvider for Fixed {
fn get(&self, tld: &Tld) -> Option<Arc<Registry>> {
self.0.get(tld).cloned()
}
fn tlds(&self) -> Vec<Tld> {
self.0.keys().cloned().collect()
}
}
#[test]
fn resolve_prefers_the_longest_suffix() {
let provider = Fixed::new(&["uk", "co.uk"]);
let name = DomainName::parse("example.co.uk").unwrap();
let resolved = provider.resolve(&name).unwrap();
assert_eq!(resolved.tld.ascii(), "co.uk");
assert_eq!(resolved.sld(), "example");
assert!(!resolved.is_subdomain());
}
#[test]
fn resolve_falls_back_to_the_shorter_suffix() {
let provider = Fixed::new(&["com"]);
let resolved = provider
.resolve(&DomainName::parse("co.com").unwrap())
.unwrap();
assert_eq!(resolved.tld.ascii(), "com");
assert_eq!(resolved.sld(), "co");
}
#[test]
fn resolve_trims_subdomains_to_the_registrable_name() {
let provider = Fixed::new(&["co.uk"]);
let name = DomainName::parse("www.mail.example.co.uk").unwrap();
let resolved = provider.resolve(&name).unwrap();
assert_eq!(resolved.registrable.as_ascii(), "example.co.uk");
assert_eq!(resolved.queried.as_ascii(), "www.mail.example.co.uk");
assert!(resolved.is_subdomain());
assert_eq!(resolved.sld(), "example");
}
#[test]
fn unsupported_tld_names_the_root_label() {
let provider = Fixed::new(&["com"]);
let error = provider
.resolve(&DomainName::parse("example.co.uk").unwrap())
.unwrap_err();
match error {
Error::UnsupportedTld { tld } => assert_eq!(tld.ascii(), "uk"),
other => panic!("expected UnsupportedTld, got {other:?}"),
}
}
#[test]
fn idn_suffix_resolves_through_either_spelling() {
let provider = Fixed::new(&["xn--p1ai"]);
for input in ["example.рф", "example.xn--p1ai"] {
let name = DomainName::parse(input).unwrap();
let resolved = provider.resolve(&name).unwrap();
assert_eq!(resolved.tld.ascii(), "xn--p1ai");
}
}
}