pub mod api;
use std::error::Error as StdErr;
use std::sync::Arc;
pub use api::{ApiRecord, Client, ClientConfig, NamecraneError};
use crate::{
CreateRecord, CreateRecordError, DeleteRecord, DeleteRecordError, HttpClientConfig, Provider,
Record, RecordData, RetrieveRecordError, RetrieveZoneError, Zone,
};
#[derive(Clone)]
pub struct NamecraneProvider {
api_client: Arc<Client>,
domain: String,
}
pub struct NamecraneZone {
api_client: Arc<Client>,
domain: String,
}
impl NamecraneZone {
pub fn domain(&self) -> &str {
&self.domain
}
}
impl NamecraneProvider {
pub fn new(config: ClientConfig) -> Result<Self, Box<dyn StdErr + Send + Sync>> {
let domain = config.domain.clone();
let api_client = Client::new(config)?;
Ok(Self {
api_client: Arc::new(api_client),
domain,
})
}
pub fn with_http_config(
config: ClientConfig,
http_config: HttpClientConfig,
) -> Result<Self, Box<dyn StdErr + Send + Sync>> {
let domain = config.domain.clone();
let api_client = Client::with_http_config(config, http_config)?;
Ok(Self {
api_client: Arc::new(api_client),
domain,
})
}
}
impl Provider for NamecraneProvider {
type Zone = NamecraneZone;
type CustomRetrieveError = NamecraneError;
async fn get_zone(
&self,
zone_id: &str,
) -> Result<Self::Zone, RetrieveZoneError<Self::CustomRetrieveError>> {
if zone_id != self.domain {
return Err(RetrieveZoneError::NotFound);
}
let zone = NamecraneZone {
api_client: self.api_client.clone(),
domain: self.domain.clone(),
};
zone.api_client.list(None).await.map_err(|e| match e {
NamecraneError::Unauthorized => RetrieveZoneError::Unauthorized,
NamecraneError::DomainNotFound => RetrieveZoneError::NotFound,
NamecraneError::Forbidden(_) => RetrieveZoneError::Unauthorized,
other => RetrieveZoneError::Custom(other),
})?;
Ok(zone)
}
async fn list_zones(
&self,
) -> Result<Vec<Self::Zone>, RetrieveZoneError<Self::CustomRetrieveError>> {
Ok(vec![])
}
}
impl Zone for NamecraneZone {
type CustomRetrieveError = NamecraneError;
fn id(&self) -> &str {
&self.domain
}
fn domain(&self) -> &str {
&self.domain
}
async fn list_records(
&self,
) -> Result<Vec<Record>, RetrieveRecordError<Self::CustomRetrieveError>> {
let api_records = self.api_client.list(None).await.map_err(|e| match e {
NamecraneError::Unauthorized => RetrieveRecordError::Unauthorized,
NamecraneError::Forbidden(_) => RetrieveRecordError::Unauthorized,
other => RetrieveRecordError::Custom(other),
})?;
let records = api_records
.into_iter()
.map(|r| api_record_to_record(r, &self.domain))
.collect();
Ok(records)
}
async fn get_record(
&self,
record_id: &str,
) -> Result<Record, RetrieveRecordError<Self::CustomRetrieveError>> {
let api_record = self.api_client.get(record_id).await.map_err(|e| match e {
NamecraneError::Unauthorized => RetrieveRecordError::Unauthorized,
NamecraneError::Forbidden(_) => RetrieveRecordError::Unauthorized,
NamecraneError::RecordNotFound => RetrieveRecordError::NotFound,
other => RetrieveRecordError::Custom(other),
})?;
Ok(api_record_to_record(api_record, &self.domain))
}
}
impl CreateRecord for NamecraneZone {
type CustomCreateError = NamecraneError;
async fn create_record(
&self,
host: &str,
data: &RecordData,
ttl: u64,
) -> Result<Record, CreateRecordError<Self::CustomCreateError>> {
let record_type = data.get_type();
let content = match data {
RecordData::MX { priority, mail_server } => {
format!("{} {}", priority, mail_server)
}
RecordData::SRV { priority, weight, port, target } => {
format!("{} {} {} {}", priority, weight, port, target)
}
_ => data.get_api_value(),
};
let record_id = self
.api_client
.create(host, record_type, &content, Some(ttl))
.await
.map_err(|e| match e {
NamecraneError::Unauthorized => CreateRecordError::Unauthorized,
NamecraneError::Forbidden(_) => CreateRecordError::Unauthorized,
other => CreateRecordError::Custom(other),
})?;
let full_host = if host == "@" {
self.domain.clone()
} else {
format!("{}.{}", host, self.domain)
};
Ok(Record {
id: record_id,
host: full_host,
data: data.clone(),
ttl,
})
}
}
impl DeleteRecord for NamecraneZone {
type CustomDeleteError = NamecraneError;
async fn delete_record(
&self,
record_id: &str,
) -> Result<(), DeleteRecordError<Self::CustomDeleteError>> {
self.api_client.delete(record_id).await.map_err(|e| match e {
NamecraneError::Unauthorized => DeleteRecordError::Unauthorized,
NamecraneError::Forbidden(_) => DeleteRecordError::Unauthorized,
NamecraneError::RecordNotFound => DeleteRecordError::NotFound,
other => DeleteRecordError::Custom(other),
})?;
Ok(())
}
}
fn api_record_to_record(api_record: ApiRecord, domain: &str) -> Record {
let host = if api_record.name == "@" {
domain.to_string()
} else {
format!("{}.{}", api_record.name, domain)
};
let data = match api_record.record_type.as_str() {
"MX" => {
let parts: Vec<&str> = api_record.content.splitn(2, ' ').collect();
if parts.len() == 2 {
if let Ok(priority) = parts[0].parse::<u16>() {
RecordData::MX {
priority,
mail_server: parts[1].to_string(),
}
} else {
RecordData::from_raw(&api_record.record_type, &api_record.content)
}
} else {
RecordData::from_raw(&api_record.record_type, &api_record.content)
}
}
"SRV" => {
let parts: Vec<&str> = api_record.content.splitn(4, ' ').collect();
if parts.len() == 4 {
if let (Ok(priority), Ok(weight), Ok(port)) = (
parts[0].parse::<u16>(),
parts[1].parse::<u16>(),
parts[2].parse::<u16>(),
) {
RecordData::SRV {
priority,
weight,
port,
target: parts[3].to_string(),
}
} else {
RecordData::from_raw(&api_record.record_type, &api_record.content)
}
} else {
RecordData::from_raw(&api_record.record_type, &api_record.content)
}
}
_ => RecordData::from_raw(&api_record.record_type, &api_record.content),
};
Record {
id: api_record.id,
host,
data,
ttl: api_record.ttl,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_record_to_record_apex() {
let api = ApiRecord {
id: "550e8400-e29b-41d4-a716-446655440000".to_string(),
name: "@".to_string(),
record_type: "A".to_string(),
content: "1.2.3.4".to_string(),
ttl: 300,
};
let record = api_record_to_record(api, "example.com");
assert_eq!(record.host, "example.com");
assert_eq!(record.id, "550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn test_api_record_to_record_subdomain() {
let api = ApiRecord {
id: "6ba7b810-9dad-11d1-80b4-00c04fd430c8".to_string(),
name: "www".to_string(),
record_type: "A".to_string(),
content: "1.2.3.4".to_string(),
ttl: 300,
};
let record = api_record_to_record(api, "example.com");
assert_eq!(record.host, "www.example.com");
}
#[test]
fn test_api_record_to_record_mx() {
let api = ApiRecord {
id: "f47ac10b-58cc-4372-a567-0e02b2c3d479".to_string(),
name: "@".to_string(),
record_type: "MX".to_string(),
content: "10 mail.example.com.".to_string(),
ttl: 3600,
};
let record = api_record_to_record(api, "example.com");
assert!(matches!(record.data, RecordData::MX { priority: 10, .. }));
}
#[test]
fn test_api_record_to_record_srv() {
let api = ApiRecord {
id: "a1b2c3d4-5678-90ab-cdef-1234567890ab".to_string(),
name: "_sip._tcp".to_string(),
record_type: "SRV".to_string(),
content: "10 5 5060 sipserver.example.com.".to_string(),
ttl: 3600,
};
let record = api_record_to_record(api, "example.com");
assert!(matches!(
record.data,
RecordData::SRV {
priority: 10,
weight: 5,
port: 5060,
..
}
));
}
}