use std::error::Error;
use std::fmt;
use std::time::Duration;
use reqwest::Client as HttpClient;
use serde::{Deserialize, Serialize};
use crate::types::Environment;
use crate::HttpClientConfig;
const PRODUCTION_API_URL: &str = "https://namecrane.com/index.php?m=craneapi";
const SANDBOX_API_URL: &str = "https://namecrane.org/index.php?m=craneapi";
#[derive(Debug)]
pub enum NamecraneError {
Request(reqwest::Error),
Api { message: String, code: u16 },
Parse(String),
Unauthorized,
DomainNotFound,
RecordNotFound,
Forbidden(String),
}
impl fmt::Display for NamecraneError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NamecraneError::Request(e) => write!(f, "HTTP request error: {}", e),
NamecraneError::Api { message, code } => write!(f, "API error ({}): {}", code, message),
NamecraneError::Parse(msg) => write!(f, "Parse error: {}", msg),
NamecraneError::Unauthorized => write!(f, "Unauthorized (invalid API key)"),
NamecraneError::DomainNotFound => write!(f, "Domain not found or not authorized"),
NamecraneError::RecordNotFound => write!(f, "Record not found"),
NamecraneError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
}
}
}
impl Error for NamecraneError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
NamecraneError::Request(e) => Some(e),
_ => None,
}
}
}
impl From<reqwest::Error> for NamecraneError {
fn from(err: reqwest::Error) -> Self {
NamecraneError::Request(err)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ApiRecord {
pub id: String,
pub name: String,
#[serde(rename = "type")]
pub record_type: String,
pub content: String,
pub ttl: u64,
}
#[derive(Debug, Deserialize)]
struct ApiResponse {
success: bool,
#[serde(default)]
records: Option<Vec<ApiRecord>>,
#[serde(default)]
record: Option<ApiRecord>,
#[serde(default)]
id: Option<String>,
#[serde(default)]
error: Option<String>,
#[serde(default)]
message: Option<String>,
#[serde(default)]
code: Option<u16>,
}
#[derive(Debug, Serialize)]
struct ListRequest<'a> {
action: &'static str,
domain: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "type")]
record_type: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<&'a str>,
}
#[derive(Debug, Serialize)]
struct CreateRequest<'a> {
action: &'static str,
domain: &'a str,
name: &'a str,
#[serde(rename = "type")]
record_type: &'a str,
content: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
ttl: Option<u64>,
}
#[derive(Debug, Serialize)]
struct DeleteRequest<'a> {
action: &'static str,
domain: &'a str,
id: &'a str,
}
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub api_key: String,
pub domain: String,
pub environment: Environment,
}
impl ClientConfig {
pub fn new(
api_key: impl Into<String>,
domain: impl Into<String>,
environment: Environment,
) -> Self {
Self {
api_key: api_key.into(),
domain: domain.into(),
environment,
}
}
pub fn sandbox(api_key: impl Into<String>, domain: impl Into<String>) -> Self {
Self::new(api_key, domain, Environment::Sandbox)
}
pub fn production(api_key: impl Into<String>, domain: impl Into<String>) -> Self {
Self::new(api_key, domain, Environment::Production)
}
pub fn api_url(&self) -> &'static str {
match self.environment {
Environment::Production => PRODUCTION_API_URL,
Environment::Sandbox => SANDBOX_API_URL,
}
}
}
#[derive(Debug, Clone)]
pub struct Client {
http_client: HttpClient,
api_key: String,
base_url: &'static str,
domain: String,
}
impl Client {
pub fn new(config: ClientConfig) -> Result<Self, Box<dyn Error + Send + Sync>> {
Self::with_http_config(config, HttpClientConfig::default())
}
pub fn with_http_config(
config: ClientConfig,
http_config: HttpClientConfig,
) -> Result<Self, Box<dyn Error + Send + Sync>> {
let mut builder = HttpClient::builder()
.user_agent("manydns-rs/1.1.1")
.timeout(http_config.timeout.unwrap_or(Duration::from_secs(30)));
if let Some(addr) = http_config.local_address {
builder = builder.local_address(addr);
}
#[cfg(any(
target_os = "android",
target_os = "fuchsia",
target_os = "linux",
target_os = "macos",
target_os = "ios",
target_os = "tvos",
target_os = "watchos",
target_os = "illumos",
target_os = "solaris",
))]
if let Some(ref iface) = http_config.interface {
builder = builder.interface(iface);
}
let http_client = builder.build()?;
let base_url = config.api_url();
Ok(Self {
http_client,
api_key: config.api_key,
base_url,
domain: config.domain,
})
}
pub fn domain(&self) -> &str {
&self.domain
}
async fn request<T: Serialize>(&self, body: &T) -> Result<ApiResponse, NamecraneError> {
let response = self
.http_client
.post(self.base_url)
.header("Authorization", format!("Bearer {}", self.api_key))
.header("Content-Type", "application/json")
.json(body)
.send()
.await?;
let status = response.status();
let text = response.text().await?;
let api_response: ApiResponse = serde_json::from_str(&text).map_err(|e| {
NamecraneError::Parse(format!("Failed to parse response: {} - {}", e, text))
})?;
if !api_response.success {
let code = api_response.code.unwrap_or(status.as_u16());
let message = api_response
.error
.or(api_response.message)
.unwrap_or_else(|| "Unknown error".to_string());
return Err(match code {
401 => NamecraneError::Unauthorized,
403 => NamecraneError::Forbidden(message),
404 => {
if message.to_lowercase().contains("domain") {
NamecraneError::DomainNotFound
} else {
NamecraneError::RecordNotFound
}
}
_ => NamecraneError::Api { message, code },
});
}
Ok(api_response)
}
pub async fn list(&self, record_type: Option<&str>) -> Result<Vec<ApiRecord>, NamecraneError> {
let request = ListRequest {
action: "dns.list",
domain: &self.domain,
record_type,
id: None,
};
let response = self.request(&request).await?;
Ok(response.records.unwrap_or_default())
}
pub async fn get(&self, record_id: &str) -> Result<ApiRecord, NamecraneError> {
let request = ListRequest {
action: "dns.list",
domain: &self.domain,
record_type: None,
id: Some(record_id),
};
let response = self.request(&request).await?;
response
.record
.or_else(|| response.records.and_then(|r| r.into_iter().next()))
.ok_or(NamecraneError::RecordNotFound)
}
pub async fn create(
&self,
name: &str,
record_type: &str,
content: &str,
ttl: Option<u64>,
) -> Result<String, NamecraneError> {
let request = CreateRequest {
action: "dns.create",
domain: &self.domain,
name,
record_type,
content,
ttl,
};
let response = self.request(&request).await?;
response
.id
.ok_or_else(|| NamecraneError::Parse("No record ID in create response".to_string()))
}
pub async fn delete(&self, record_id: &str) -> Result<(), NamecraneError> {
let request = DeleteRequest {
action: "dns.delete",
domain: &self.domain,
id: record_id,
};
self.request(&request).await?;
Ok(())
}
}