use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, OnceLock};
use serde::Deserialize;
use crate::domain::Tld;
use crate::error::{Error, Result};
use crate::registry::{Endpoint, IdnForm, Registry, RegistryProvider};
const BUNDLED_JSON: &str = include_str!("../../data/registries.json");
const SUPPORTED_SCHEMA: u32 = 1;
#[derive(Debug, Clone)]
pub struct JsonRegistry {
by_tld: HashMap<Tld, Arc<Registry>>,
origin: String,
}
impl JsonRegistry {
pub fn from_json(json: &str, origin: impl Into<String>) -> Result<Self> {
let origin = origin.into();
let file: RegistryFile = serde_json::from_str(json)
.map_err(|error| Error::Definitions(format!("{origin}: {error}")))?;
if file.schema > SUPPORTED_SCHEMA {
return Err(Error::Definitions(format!(
"{origin}: schema version {} is newer than this build understands ({SUPPORTED_SCHEMA})",
file.schema
)));
}
let mut by_tld: HashMap<Tld, Arc<Registry>> = HashMap::new();
for (index, spec) in file.registries.into_iter().enumerate() {
let context = |detail: String| {
Error::Definitions(format!("{origin}: registries[{index}]: {detail}"))
};
let tlds = spec.parse_tlds().map_err(&context)?;
let endpoints = spec.parse_endpoints().map_err(&context)?;
let mut builder = Registry::builder(tlds.clone())
.endpoints(endpoints)
.available_markers(spec.available)
.premium_markers(spec.premium)
.thin(spec.thin)
.idn_form(spec.idn)
.available_when_empty(spec.available_when_empty);
if let Some(note) = spec.note {
builder = builder.note(note);
}
let registry = builder.build_shared();
for tld in tlds {
by_tld.insert(tld, Arc::clone(®istry));
}
}
if by_tld.is_empty() {
return Err(Error::Definitions(format!(
"{origin}: no registries defined"
)));
}
Ok(JsonRegistry { by_tld, origin })
}
pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let json = std::fs::read_to_string(path)
.map_err(|error| Error::Definitions(format!("{}: {error}", path.display())))?;
JsonRegistry::from_json(&json, path.display().to_string())
}
pub fn bundled() -> Arc<JsonRegistry> {
Arc::clone(bundled_cell())
}
pub fn try_bundled() -> Result<Self> {
JsonRegistry::from_json(BUNDLED_JSON, "bundled data/registries.json")
}
pub fn origin(&self) -> &str {
&self.origin
}
pub fn len(&self) -> usize {
self.by_tld.len()
}
pub fn is_empty(&self) -> bool {
self.by_tld.is_empty()
}
pub fn registries(&self) -> Vec<Arc<Registry>> {
let mut seen = Vec::new();
for registry in self.by_tld.values() {
if !seen.iter().any(|other| Arc::ptr_eq(other, registry)) {
seen.push(Arc::clone(registry));
}
}
seen
}
}
fn bundled_cell() -> &'static Arc<JsonRegistry> {
static BUNDLED: OnceLock<Arc<JsonRegistry>> = OnceLock::new();
BUNDLED.get_or_init(|| {
Arc::new(
JsonRegistry::try_bundled()
.expect("bundled data/registries.json is not a valid definition file"),
)
})
}
impl RegistryProvider for JsonRegistry {
fn get(&self, tld: &Tld) -> Option<Arc<Registry>> {
self.by_tld.get(tld).map(Arc::clone)
}
fn tlds(&self) -> Vec<Tld> {
let mut tlds: Vec<Tld> = self.by_tld.keys().cloned().collect();
tlds.sort();
tlds
}
fn describe(&self) -> String {
format!("{} ({} tlds)", self.origin, self.by_tld.len())
}
}
#[derive(Debug, Deserialize)]
struct RegistryFile {
#[serde(default = "default_schema")]
schema: u32,
#[serde(default)]
registries: Vec<RegistrySpec>,
}
fn default_schema() -> u32 {
SUPPORTED_SCHEMA
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct RegistrySpec {
tlds: Vec<String>,
#[serde(default)]
whois: Option<String>,
#[serde(default)]
rdap: Vec<String>,
#[serde(default)]
available: Vec<String>,
#[serde(default)]
premium: Vec<String>,
#[serde(default)]
thin: bool,
#[serde(default)]
idn: IdnForm,
#[serde(default)]
available_when_empty: bool,
#[serde(default)]
note: Option<String>,
}
impl RegistrySpec {
fn parse_tlds(&self) -> std::result::Result<Vec<Tld>, String> {
if self.tlds.is_empty() {
return Err("no tlds listed".into());
}
let mut tlds = Vec::with_capacity(self.tlds.len());
for raw in &self.tlds {
let tld = Tld::parse(raw).map_err(|error| error.to_string())?;
if !tlds.contains(&tld) {
tlds.push(tld);
}
}
Ok(tlds)
}
fn parse_endpoints(&self) -> std::result::Result<Vec<Endpoint>, String> {
let mut endpoints = Vec::new();
if let Some(whois) = self
.whois
.as_deref()
.map(str::trim)
.filter(|s| !s.is_empty())
{
endpoints.push(Endpoint::parse(whois).map_err(|error| error.to_string())?);
}
for rdap in &self.rdap {
if rdap.trim().is_empty() {
continue;
}
endpoints.push(Endpoint::parse(rdap).map_err(|error| error.to_string())?);
}
Ok(endpoints)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bundled_data_parses_and_covers_the_majors() {
let registry = JsonRegistry::bundled();
assert!(
registry.len() > 800,
"expected the full curated list, got {}",
registry.len()
);
for tld in ["com", "net", "org", "ir", "co.uk", "de"] {
assert!(
registry.get(&Tld::parse(tld).unwrap()).is_some(),
"bundled data is missing .{tld}"
);
}
}
#[test]
fn bundled_com_and_net_share_one_definition() {
let registry = JsonRegistry::bundled();
let com = registry.get(&Tld::parse("com").unwrap()).unwrap();
let net = registry.get(&Tld::parse("net").unwrap()).unwrap();
assert!(Arc::ptr_eq(&com, &net));
assert!(com.is_thin());
}
#[test]
fn bundled_data_covers_suffixes_the_upstream_list_lacks() {
let registry = JsonRegistry::bundled();
let tr = registry.get(&Tld::parse("tr").unwrap()).unwrap();
assert_eq!(tr.endpoints()[0].address(), "whois.trabis.gov.tr");
assert_eq!(tr.available_markers(), ["No match found for"]);
}
#[test]
fn bundled_hosts_are_the_current_ones_not_legacy_aliases() {
let registry = JsonRegistry::bundled();
let com = registry.get(&Tld::parse("com").unwrap()).unwrap();
assert_eq!(com.endpoints()[0].address(), "whois.verisign-grs.com");
}
#[test]
fn bundled_de_asks_for_unicode() {
let registry = JsonRegistry::bundled();
let de = registry.get(&Tld::parse("de").unwrap()).unwrap();
assert_eq!(de.idn_form(), IdnForm::Unicode);
}
#[test]
fn bundled_registries_all_have_an_endpoint() {
let registry = JsonRegistry::bundled();
for definition in registry.registries() {
assert!(
!definition.endpoints().is_empty(),
"{:?} has no endpoint",
definition.tlds()
);
}
}
#[test]
fn parses_a_minimal_document() {
let json = r#"{"registries":[{"tlds":[".EXAMPLE"],"whois":"whois.nic.example"}]}"#;
let registry = JsonRegistry::from_json(json, "test").unwrap();
let entry = registry.get(&Tld::parse("example").unwrap()).unwrap();
assert_eq!(entry.endpoints()[0].address(), "whois.nic.example");
assert!(entry.available_markers().is_empty());
assert!(!entry.is_thin());
}
#[test]
fn whois_is_tried_before_rdap() {
let json = r#"{"registries":[{
"tlds":["example"],
"whois":"whois.nic.example",
"rdap":["https://rdap.example/"]
}]}"#;
let registry = JsonRegistry::from_json(json, "test").unwrap();
let entry = registry.get(&Tld::parse("example").unwrap()).unwrap();
assert!(entry.endpoints()[0].is_whois());
assert!(entry.endpoints()[1].is_rdap());
}
#[test]
fn note_survives_the_build() {
let json = r#"{"registries":[{"tlds":["example"],"whois":"w.example","note":"why"}]}"#;
let registry = JsonRegistry::from_json(json, "test").unwrap();
let entry = registry.get(&Tld::parse("example").unwrap()).unwrap();
assert_eq!(entry.note(), Some("why"));
assert_eq!(entry.endpoints().len(), 1);
}
#[test]
fn later_definitions_win() {
let json = r#"{"registries":[
{"tlds":["example"],"whois":"old.example"},
{"tlds":["example"],"whois":"new.example"}
]}"#;
let registry = JsonRegistry::from_json(json, "test").unwrap();
let entry = registry.get(&Tld::parse("example").unwrap()).unwrap();
assert_eq!(entry.endpoints()[0].address(), "new.example");
}
#[test]
fn rejects_documents_it_cannot_trust() {
let cases = [
("not json", "{"),
("empty", r#"{"registries":[]}"#),
("no tlds", r#"{"registries":[{"tlds":[]}]}"#),
("bad tld", r#"{"registries":[{"tlds":["-nope"]}]}"#),
(
"bad endpoint",
r#"{"registries":[{"tlds":["x"],"whois":"ftp://w"}]}"#,
),
(
"future schema",
r#"{"schema":99,"registries":[{"tlds":["x"]}]}"#,
),
(
"unknown field",
r#"{"registries":[{"tlds":["x"],"typo":1}]}"#,
),
];
for (name, json) in cases {
let error = JsonRegistry::from_json(json, "test").unwrap_err();
assert!(
matches!(error, Error::Definitions(_)),
"{name}: expected Definitions, got {error:?}"
);
}
}
#[test]
fn error_messages_point_at_the_bad_entry() {
let json = r#"{"registries":[{"tlds":["ok"]},{"tlds":["-bad"]}]}"#;
let error = JsonRegistry::from_json(json, "my-file.json").unwrap_err();
let message = error.to_string();
assert!(message.contains("my-file.json"), "{message}");
assert!(message.contains("registries[1]"), "{message}");
}
#[test]
fn tlds_are_listed_shortest_suffix_first() {
let json = r#"{"registries":[{"tlds":["co.uk","uk","com"],"whois":"w.example"}]}"#;
let registry = JsonRegistry::from_json(json, "test").unwrap();
let listed: Vec<String> = registry
.tlds()
.iter()
.map(|t| t.ascii().to_string())
.collect();
assert_eq!(listed, ["com", "uk", "co.uk"]);
}
}