use async_trait::async_trait;
use futures::stream::{self, StreamExt};
use crate::error::{ProviderError, Result};
use crate::types::{
BatchCreateFailure, BatchCreateResult, BatchDeleteFailure, BatchDeleteResult,
BatchUpdateFailure, BatchUpdateItem, BatchUpdateResult, CreateDnsRecordRequest, DnsRecord,
PaginatedResponse, PaginationParams, ProviderDomain, ProviderMetadata, RecordQueryParams,
UpdateDnsRecordRequest,
};
const DEFAULT_BATCH_CONCURRENCY: usize = 5;
#[derive(Debug, Clone)]
pub(crate) struct RawApiError {
pub code: Option<String>,
pub message: String,
}
impl RawApiError {
pub fn new(message: impl Into<String>) -> Self {
Self {
code: None,
message: message.into(),
}
}
pub fn with_code(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: Some(code.into()),
message: message.into(),
}
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct ErrorContext {
pub record_name: Option<String>,
pub record_id: Option<String>,
pub domain: Option<String>,
}
pub(crate) trait ProviderErrorMapper {
fn provider_name(&self) -> &'static str;
fn map_error(&self, raw: RawApiError, context: ErrorContext) -> ProviderError;
fn parse_error(&self, detail: impl ToString) -> ProviderError {
ProviderError::ParseError {
provider: self.provider_name().to_string(),
detail: detail.to_string(),
}
}
fn unknown_error(&self, raw: RawApiError) -> ProviderError {
ProviderError::Unknown {
provider: self.provider_name().to_string(),
raw_code: raw.code,
raw_message: raw.message,
}
}
}
#[async_trait]
pub trait DnsProvider: Send + Sync {
fn id(&self) -> &'static str;
fn metadata() -> ProviderMetadata
where
Self: Sized;
async fn validate_credentials(&self) -> Result<bool>;
async fn list_domains(
&self,
params: &PaginationParams,
) -> Result<PaginatedResponse<ProviderDomain>>;
async fn get_domain(&self, domain_id: &str) -> Result<ProviderDomain>;
async fn list_records(
&self,
domain_id: &str,
params: &RecordQueryParams,
) -> Result<PaginatedResponse<DnsRecord>>;
async fn create_record(&self, req: &CreateDnsRecordRequest) -> Result<DnsRecord>;
async fn update_record(
&self,
record_id: &str,
req: &UpdateDnsRecordRequest,
) -> Result<DnsRecord>;
async fn delete_record(&self, record_id: &str, domain_id: &str) -> Result<()>;
async fn batch_create_records(
&self,
requests: &[CreateDnsRecordRequest],
) -> Result<BatchCreateResult> {
let indexed_requests: Vec<_> = requests
.iter()
.enumerate()
.map(|(i, req)| (i, req.clone()))
.collect();
let results: Vec<(usize, std::result::Result<DnsRecord, ProviderError>)> =
stream::iter(indexed_requests)
.map(|(i, req)| async move { (i, self.create_record(&req).await) })
.buffer_unordered(DEFAULT_BATCH_CONCURRENCY)
.collect()
.await;
let mut created_records = Vec::new();
let mut failures = Vec::new();
for (i, result) in results {
match result {
Ok(record) => created_records.push(record),
Err(e) => failures.push(BatchCreateFailure {
request_index: i,
record_name: requests[i].name.clone(),
reason: e.to_string(),
}),
}
}
Ok(BatchCreateResult {
success_count: created_records.len(),
failed_count: failures.len(),
created_records,
failures,
})
}
async fn batch_update_records(&self, updates: &[BatchUpdateItem]) -> Result<BatchUpdateResult> {
let indexed_updates: Vec<_> = updates
.iter()
.enumerate()
.map(|(i, item)| (i, item.record_id.clone(), item.request.clone()))
.collect();
let results: Vec<(usize, std::result::Result<DnsRecord, ProviderError>)> =
stream::iter(indexed_updates)
.map(|(i, record_id, req)| async move {
(i, self.update_record(&record_id, &req).await)
})
.buffer_unordered(DEFAULT_BATCH_CONCURRENCY)
.collect()
.await;
let mut updated_records = Vec::new();
let mut failures = Vec::new();
for (i, result) in results {
match result {
Ok(record) => updated_records.push(record),
Err(e) => failures.push(BatchUpdateFailure {
record_id: updates[i].record_id.clone(),
reason: e.to_string(),
}),
}
}
Ok(BatchUpdateResult {
success_count: updated_records.len(),
failed_count: failures.len(),
updated_records,
failures,
})
}
async fn batch_delete_records(
&self,
domain_id: &str,
record_ids: &[String],
) -> Result<BatchDeleteResult> {
let indexed_ids: Vec<_> = record_ids
.iter()
.enumerate()
.map(|(i, id)| (i, id.clone()))
.collect();
let domain_id_owned = domain_id.to_string();
let results: Vec<(usize, std::result::Result<(), ProviderError>)> =
stream::iter(indexed_ids)
.map(|(i, id)| {
let domain_id = &domain_id_owned;
async move { (i, self.delete_record(&id, domain_id).await) }
})
.buffer_unordered(DEFAULT_BATCH_CONCURRENCY)
.collect()
.await;
let mut success_count = 0;
let mut failures = Vec::new();
for (i, result) in results {
match result {
Ok(()) => success_count += 1,
Err(e) => failures.push(BatchDeleteFailure {
record_id: record_ids[i].clone(),
reason: e.to_string(),
}),
}
}
Ok(BatchDeleteResult {
success_count,
failed_count: failures.len(),
failures,
})
}
}