From 506d4c437923e39862c95b455bd7c03852fac8af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 05:48:56 +0000 Subject: [PATCH 1/4] Initial plan From f617331e75c7d0a3d58b4166c8f8806d077cc741 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 05:51:48 +0000 Subject: [PATCH 2/4] feat: add unified domain registrar Rust library Agent-Logs-Url: https://github.com/trydirect/registrar/sessions/67657097-b19c-4c9e-80e6-e62c4f364ee4 Co-authored-by: vsilent <42473+vsilent@users.noreply.github.com> --- Cargo.lock | 7 ++ Cargo.toml | 6 ++ README.md | 7 +- src/lib.rs | 187 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/lib.rs 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..d8fdd16 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,187 @@ +use std::error::Error; +use std::fmt; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RegistrarError { + NoRegistrarsConfigured, + 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::DomainUnavailable => write!(f, "domain is unavailable across all registrars"), + Self::ProviderFailure(message) => write!(f, "registrar 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; + + for registrar in &self.registrars { + match registrar.is_domain_available(domain) { + Ok(true) => return Ok(true), + Ok(false) => saw_successful_check = true, + Err(_) => continue, + } + } + + if saw_successful_check { + Ok(false) + } else { + Err(RegistrarError::ProviderFailure( + "all registrars failed availability checks".to_string(), + )) + } + } + + pub fn register_domain(&self, domain: &str, years: u8) -> Result<&str, RegistrarError> { + if self.registrars.is_empty() { + return Err(RegistrarError::NoRegistrarsConfigured); + } + + 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()), + 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".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")); + } + + #[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 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) + ); + } +} From c329922d95c555bce56ae77e15d1d216c976dcae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 05:54:31 +0000 Subject: [PATCH 3/4] test: cover registrar failure fallback paths Agent-Logs-Url: https://github.com/trydirect/registrar/sessions/67657097-b19c-4c9e-80e6-e62c4f364ee4 Co-authored-by: vsilent <42473+vsilent@users.noreply.github.com> --- src/lib.rs | 49 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d8fdd16..500ef68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,7 +63,7 @@ impl UnifiedRegistrar { } } - pub fn register_domain(&self, domain: &str, years: u8) -> Result<&str, RegistrarError> { + pub fn register_domain(&self, domain: &str, years: u8) -> Result { if self.registrars.is_empty() { return Err(RegistrarError::NoRegistrarsConfigured); } @@ -74,7 +74,7 @@ impl UnifiedRegistrar { for registrar in &self.registrars { match registrar.is_domain_available(domain) { Ok(true) => match registrar.register_domain(domain, years) { - Ok(()) => return Ok(registrar.name()), + Ok(()) => return Ok(registrar.name().to_string()), Err(error) => last_error = Some(error), }, Ok(false) => saw_unavailable = true, @@ -155,7 +155,10 @@ mod tests { Box::new(MockRegistrar::new("two", Ok(true), Ok(()))), ]); - assert_eq!(registrar.register_domain("example.com", 1), Ok("two")); + assert_eq!( + registrar.register_domain("example.com", 1), + Ok("two".to_string()) + ); } #[test] @@ -171,6 +174,46 @@ mod tests { ); } + #[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".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()); From d5c9e268a2f2ee0078c20d4a4fd30ef6b65e1d3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 05:56:04 +0000 Subject: [PATCH 4/4] refactor: tighten registrar error handling and input validation Agent-Logs-Url: https://github.com/trydirect/registrar/sessions/67657097-b19c-4c9e-80e6-e62c4f364ee4 Co-authored-by: vsilent <42473+vsilent@users.noreply.github.com> --- src/lib.rs | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 500ef68..fa0c75f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ use std::fmt; #[derive(Debug, Clone, PartialEq, Eq)] pub enum RegistrarError { NoRegistrarsConfigured, + InvalidRegistrationPeriod(u8), DomainUnavailable, ProviderFailure(String), } @@ -12,8 +13,11 @@ 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, "registrar failure: {message}"), + Self::ProviderFailure(message) => write!(f, "provider failure: {message}"), } } } @@ -45,17 +49,22 @@ impl UnifiedRegistrar { } 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(_) => continue, + 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(), @@ -67,6 +76,9 @@ impl UnifiedRegistrar { 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; @@ -88,7 +100,7 @@ impl UnifiedRegistrar { Err(RegistrarError::DomainUnavailable) } else { Err(RegistrarError::ProviderFailure( - "all registrars failed".to_string(), + "all registrars failed during registration attempt".to_string(), )) } } @@ -192,7 +204,7 @@ mod tests { assert_eq!( registrar.is_domain_available("example.com"), Err(RegistrarError::ProviderFailure( - "all registrars failed availability checks".to_string() + "all registrars failed availability checks: provider failure: b".to_string() )) ); } @@ -227,4 +239,22 @@ mod tests { 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)) + ); + } }