diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1ffe135 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "registrar" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2e48774 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "registrar" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/README.md b/README.md index fb8bd94..63d62f0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ # registrar -Domain registrar manager + +Unified domain registrar library. + +This crate provides a `DomainRegistrar` trait and a `UnifiedRegistrar` manager +that can route domain availability checks and registrations across multiple +registrar providers. diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..fa0c75f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,260 @@ +use std::error::Error; +use std::fmt; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RegistrarError { + NoRegistrarsConfigured, + InvalidRegistrationPeriod(u8), + DomainUnavailable, + ProviderFailure(String), +} + +impl fmt::Display for RegistrarError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NoRegistrarsConfigured => write!(f, "no registrars configured"), + Self::InvalidRegistrationPeriod(years) => { + write!(f, "invalid registration period: {years} years") + } + Self::DomainUnavailable => write!(f, "domain is unavailable across all registrars"), + Self::ProviderFailure(message) => write!(f, "provider failure: {message}"), + } + } +} + +impl Error for RegistrarError {} + +pub trait DomainRegistrar { + fn name(&self) -> &str; + fn is_domain_available(&self, domain: &str) -> Result; + fn register_domain(&self, domain: &str, years: u8) -> Result<(), RegistrarError>; +} + +pub struct UnifiedRegistrar { + registrars: Vec>, +} + +impl UnifiedRegistrar { + pub fn new(registrars: Vec>) -> Self { + Self { registrars } + } + + pub fn add_registrar(&mut self, registrar: Box) { + self.registrars.push(registrar); + } + + pub fn is_domain_available(&self, domain: &str) -> Result { + if self.registrars.is_empty() { + return Err(RegistrarError::NoRegistrarsConfigured); + } + + let mut saw_successful_check = false; + let mut last_error = None; + + for registrar in &self.registrars { + match registrar.is_domain_available(domain) { + Ok(true) => return Ok(true), + Ok(false) => saw_successful_check = true, + Err(error) => last_error = Some(error), + } + } + + if saw_successful_check { + Ok(false) + } else if let Some(error) = last_error { + Err(RegistrarError::ProviderFailure(format!( + "all registrars failed availability checks: {error}" + ))) + } else { + Err(RegistrarError::ProviderFailure( + "all registrars failed availability checks".to_string(), + )) + } + } + + pub fn register_domain(&self, domain: &str, years: u8) -> Result { + if self.registrars.is_empty() { + return Err(RegistrarError::NoRegistrarsConfigured); + } + if years == 0 || years > 10 { + return Err(RegistrarError::InvalidRegistrationPeriod(years)); + } + + let mut saw_unavailable = false; + let mut last_error = None; + + for registrar in &self.registrars { + match registrar.is_domain_available(domain) { + Ok(true) => match registrar.register_domain(domain, years) { + Ok(()) => return Ok(registrar.name().to_string()), + Err(error) => last_error = Some(error), + }, + Ok(false) => saw_unavailable = true, + Err(error) => last_error = Some(error), + } + } + + if let Some(error) = last_error { + Err(error) + } else if saw_unavailable { + Err(RegistrarError::DomainUnavailable) + } else { + Err(RegistrarError::ProviderFailure( + "all registrars failed during registration attempt".to_string(), + )) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::cell::RefCell; + + struct MockRegistrar { + name: &'static str, + available: Result, + register_result: Result<(), RegistrarError>, + registrations: RefCell>, + } + + impl MockRegistrar { + fn new( + name: &'static str, + available: Result, + register_result: Result<(), RegistrarError>, + ) -> Self { + Self { + name, + available, + register_result, + registrations: RefCell::new(Vec::new()), + } + } + } + + impl DomainRegistrar for MockRegistrar { + fn name(&self) -> &str { + self.name + } + + fn is_domain_available(&self, _domain: &str) -> Result { + self.available.clone() + } + + fn register_domain(&self, domain: &str, years: u8) -> Result<(), RegistrarError> { + self.registrations + .borrow_mut() + .push((domain.to_string(), years)); + self.register_result.clone() + } + } + + #[test] + fn availability_returns_true_if_any_provider_has_domain() { + let registrar = UnifiedRegistrar::new(vec![ + Box::new(MockRegistrar::new("one", Ok(false), Ok(()))), + Box::new(MockRegistrar::new("two", Ok(true), Ok(()))), + ]); + + assert_eq!(registrar.is_domain_available("example.com"), Ok(true)); + } + + #[test] + fn register_uses_first_provider_that_can_register() { + let registrar = UnifiedRegistrar::new(vec![ + Box::new(MockRegistrar::new("one", Ok(false), Ok(()))), + Box::new(MockRegistrar::new("two", Ok(true), Ok(()))), + ]); + + assert_eq!( + registrar.register_domain("example.com", 1), + Ok("two".to_string()) + ); + } + + #[test] + fn register_returns_unavailable_when_all_reject_domain() { + let registrar = UnifiedRegistrar::new(vec![ + Box::new(MockRegistrar::new("one", Ok(false), Ok(()))), + Box::new(MockRegistrar::new("two", Ok(false), Ok(()))), + ]); + + assert_eq!( + registrar.register_domain("example.com", 1), + Err(RegistrarError::DomainUnavailable) + ); + } + + #[test] + fn availability_returns_failure_when_all_providers_error() { + let registrar = UnifiedRegistrar::new(vec![ + Box::new(MockRegistrar::new( + "one", + Err(RegistrarError::ProviderFailure("a".to_string())), + Ok(()), + )), + Box::new(MockRegistrar::new( + "two", + Err(RegistrarError::ProviderFailure("b".to_string())), + Ok(()), + )), + ]); + + assert_eq!( + registrar.is_domain_available("example.com"), + Err(RegistrarError::ProviderFailure( + "all registrars failed availability checks: provider failure: b".to_string() + )) + ); + } + + #[test] + fn register_falls_back_when_first_available_provider_fails_registration() { + let registrar = UnifiedRegistrar::new(vec![ + Box::new(MockRegistrar::new( + "one", + Ok(true), + Err(RegistrarError::ProviderFailure("unable to register".to_string())), + )), + Box::new(MockRegistrar::new("two", Ok(true), Ok(()))), + ]); + + assert_eq!( + registrar.register_domain("example.com", 1), + Ok("two".to_string()) + ); + } + + #[test] + fn empty_unified_registrar_returns_configuration_error() { + let registrar = UnifiedRegistrar::new(Vec::new()); + + assert_eq!( + registrar.is_domain_available("example.com"), + Err(RegistrarError::NoRegistrarsConfigured) + ); + assert_eq!( + registrar.register_domain("example.com", 1), + Err(RegistrarError::NoRegistrarsConfigured) + ); + } + + #[test] + fn register_rejects_invalid_registration_period() { + let registrar = UnifiedRegistrar::new(vec![Box::new(MockRegistrar::new( + "one", + Ok(true), + Ok(()), + ))]); + + assert_eq!( + registrar.register_domain("example.com", 0), + Err(RegistrarError::InvalidRegistrationPeriod(0)) + ); + assert_eq!( + registrar.register_domain("example.com", 11), + Err(RegistrarError::InvalidRegistrationPeriod(11)) + ); + } +}