diff --git a/Cargo.lock b/Cargo.lock index 296d844..c8ce80f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -288,7 +288,6 @@ dependencies = [ name = "dnrs" version = "0.1.0" dependencies = [ - "anyhow", "async-trait", "clap", "dirs", diff --git a/Cargo.toml b/Cargo.toml index 4cc7536..f773f4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ opt-level = 0 lto = false [dependencies] -anyhow = "1.0.99" async-trait = "0.1.89" clap = { version = "4.5.39", features = ["derive", "unicode", "wrap_help"] } dirs = "6.0.0" diff --git a/src/cli/generate_config.rs b/src/cli/generate_config.rs index 2a9b829..6668b19 100644 --- a/src/cli/generate_config.rs +++ b/src/cli/generate_config.rs @@ -4,7 +4,7 @@ use clap::Parser; use lum_log::info; use thiserror::Error; -use crate::{Config, cli::ExecutableCommand}; +use crate::{Config, ConfigError, cli::ExecutableCommand}; #[derive(Debug)] pub struct Input<'config> { @@ -20,7 +20,7 @@ pub enum Error { Yaml(#[from] serde_yaml_ng::Error), #[error("Config error: {0}")] - Config(#[from] anyhow::Error), + Config(#[from] ConfigError), } /// Generate configuration directory structure diff --git a/src/cli/get.rs b/src/cli/get.rs index a90dcef..ceec39e 100644 --- a/src/cli/get.rs +++ b/src/cli/get.rs @@ -9,7 +9,7 @@ use crate::{ cli::ExecutableCommand, config::provider::Provider as ProviderConfig, provider::{ - GetAllRecordsInput, GetRecordsInput, Provider, hetzner::HetznerProvider, + GetAllRecordsInput, GetRecordsInput, Provider, ProviderError, hetzner::HetznerProvider, netcup::NetcupProvider, nitrado::NitradoProvider, }, }; @@ -26,7 +26,13 @@ pub enum Error { ProviderNotConfigured(String), #[error("Provider error: {0}")] - ProviderError(#[from] anyhow::Error), + Provider(#[from] ProviderError), + + #[error("Cannot specify both --all and specific subdomains")] + ConflictingArguments, + + #[error("Must specify either --all or specific subdomains")] + MissingArguments, } #[derive(Debug, Args)] @@ -94,16 +100,12 @@ impl<'command> ExecutableCommand<'command> for Command<'command> { async fn execute(&self, input: &'command Self::I) -> Self::R { if self.subdomain_args.all && !self.subdomain_args.subdomains.is_empty() { error!("Cannot specify both --all and specific subdomains"); - return Err(Error::ProviderError(anyhow::anyhow!( - "Cannot specify both --all and specific subdomains" - ))); + return Err(Error::ConflictingArguments); } if !self.subdomain_args.all && self.subdomain_args.subdomains.is_empty() { error!("Must specify either --all or specific subdomains"); - return Err(Error::ProviderError(anyhow::anyhow!( - "Must specify either --all or specific subdomains" - ))); + return Err(Error::MissingArguments); } let config = input.config; diff --git a/src/config.rs b/src/config.rs index 3f54141..f5d1b36 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,9 +1,8 @@ -//TODO: No anyhow -use anyhow::Result; use lum_config::MergeFrom; use lum_libs::serde::{Deserialize, Serialize}; -use lum_log::{debug, error, info}; -use std::{fs, path::Path}; +use lum_log::{debug, info}; +use std::{fs, io, path::Path}; +use thiserror::Error; use crate::{ config::provider::Provider, @@ -14,6 +13,22 @@ pub mod dns; pub mod provider; pub mod resolver; +/// Error type for configuration loading and parsing. +#[derive(Debug, Error)] +pub enum ConfigError { + #[error("IO error: {0}")] + Io(#[from] io::Error), + + #[error("YAML parsing error: {0}")] + Yaml(#[from] serde_yaml_ng::Error), + + #[error("Unknown provider config file: {0}")] + UnknownProvider(String), + + #[error("Cannot determine DNS config type for file: {0}")] + UnknownDnsType(String), +} + /// Configuration for the dnrs application. /// /// This struct holds all the configuration required to run the application, @@ -28,11 +43,11 @@ pub struct Config { } impl Config { - pub fn load_from_directory(config_dir: impl AsRef) -> Result { + pub fn load_from_directory(config_dir: impl AsRef) -> Result { let config_dir = config_dir.as_ref(); let resolver = Self::load_resolver_config(config_dir)?; - let providers = Self::load_provider_configs(&config_dir.join("providers"))?; - let dns = Self::load_dns_configs(&config_dir.join("dns"))?; + let providers = Self::load_provider_configs(config_dir.join("providers"))?; + let dns = Self::load_dns_configs(config_dir.join("dns"))?; let loaded_config = Config { resolver, @@ -44,7 +59,7 @@ impl Config { Ok(default_config.merge_from(loaded_config)) } - fn load_resolver_config(config_dir: impl AsRef) -> Result { + fn load_resolver_config(config_dir: impl AsRef) -> Result { let resolver_path = config_dir.as_ref().join("resolver.yaml"); //TODO: Fail with error if resolver config is missing @@ -56,7 +71,9 @@ impl Config { } } - fn load_provider_configs(providers_dir: impl AsRef) -> Result> { + fn load_provider_configs( + providers_dir: impl AsRef, + ) -> Result, ConfigError> { let providers_dir = providers_dir.as_ref(); //TODO: Fail with error if providers config is missing if !providers_dir.exists() { @@ -78,7 +95,7 @@ impl Config { if path .extension() - .map_or(false, |ext| ext == "yaml" || ext == "yml") + .is_some_and(|ext| ext == "yaml" || ext == "yml") { let content = fs::read_to_string(&path)?; @@ -105,7 +122,7 @@ impl Config { debug!("Loaded Netcup provider config from {:?}", path); } _ => { - error!("Unknown provider config file: {}", path.display()); + return Err(ConfigError::UnknownProvider(path.display().to_string())); } } } @@ -120,7 +137,7 @@ impl Config { Ok(configs) } - fn load_dns_configs(dns_dir: impl AsRef) -> Result> { + fn load_dns_configs(dns_dir: impl AsRef) -> Result, ConfigError> { let dns_dir = dns_dir.as_ref(); //TODO: Fail with error if dns config is missing @@ -139,7 +156,7 @@ impl Config { if path .extension() - .map_or(false, |ext| ext == "yaml" || ext == "yml") + .is_some_and(|ext| ext == "yaml" || ext == "yml") { let content = fs::read_to_string(&path)?; @@ -162,10 +179,7 @@ impl Config { configs.push(dns::Type::Netcup(config)); debug!("Loaded Netcup DNS config from {:?}", path); } else { - error!( - "Cannot determine DNS config type for file: {}", - path.display() - ); + return Err(ConfigError::UnknownDnsType(path.display().to_string())); } } } @@ -174,7 +188,7 @@ impl Config { Ok(configs) } - pub fn create_example_structure(config_dir: impl AsRef) -> Result<()> { + pub fn create_example_structure(config_dir: impl AsRef) -> Result<(), ConfigError> { let config_dir = config_dir.as_ref(); fs::create_dir_all(config_dir.join("providers"))?; @@ -271,12 +285,10 @@ mod tests { let default_config = Config::default(); let other = Config { resolver: resolver::Config::default(), - providers: vec![Provider::Nitrado( - nitrado::Config { - name: "OtherNitrado".to_string(), - ..Default::default() - }, - )], + providers: vec![Provider::Nitrado(nitrado::Config { + name: "OtherNitrado".to_string(), + ..Default::default() + })], dns: vec![], }; diff --git a/src/lib.rs b/src/lib.rs index e3f2b78..87cc07b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,7 @@ pub mod types; #[cfg(test)] mod cli_tests; -pub use config::Config; +pub use config::{Config, ConfigError}; pub use logger::setup_logger; pub const PROGRAM_NAME: &str = env!("CARGO_PKG_NAME"); diff --git a/src/main.rs b/src/main.rs index 6cbd0de..9b77079 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::fmt::{self, Debug}; use std::fs; -use dnrs::{Config, RuntimeError, run, setup_logger}; +use dnrs::{Config, ConfigError, RuntimeError, run, setup_logger}; use lum_config::{ConfigPathError, EnvironmentConfigParseError, FileConfigParseError}; use lum_log::{info, log::SetLoggerError}; use thiserror::Error; @@ -59,7 +59,7 @@ enum Error { Io(#[from] std::io::Error), #[error("Config error: {0}")] - Config(#[from] anyhow::Error), + Config(#[from] ConfigError), #[error("Unable to determine config directory")] NoConfigDirectory, @@ -78,22 +78,22 @@ impl Debug for Error { } } -fn read_config() -> Result { +fn read_config() -> Result> { let config_dir = dirs::config_dir() - .ok_or(Error::NoConfigDirectory)? + .ok_or(Box::new(Error::NoConfigDirectory))? .join(APP_NAME); if config_dir.exists() && !config_dir.is_dir() { - return Err(Error::ConfigIsNotDirectory); + return Err(Box::new(Error::ConfigIsNotDirectory)); } let config = if config_dir.exists() { - Config::load_from_directory(&config_dir)? + Config::load_from_directory(&config_dir).map_err(|e| Box::new(Error::from(e)))? } else { info!("Config directory does not exist, creating default structure..."); - fs::create_dir_all(&config_dir)?; + fs::create_dir_all(&config_dir).map_err(|e| Box::new(Error::from(e)))?; - Config::create_example_structure(&config_dir)?; + Config::create_example_structure(&config_dir).map_err(|e| Box::new(Error::from(e)))?; info!( "Created default config structure at: {}", config_dir.display() @@ -108,11 +108,11 @@ fn read_config() -> Result { } #[tokio::main] -async fn main() -> Result<(), Error> { - setup_logger()?; +async fn main() -> Result<(), Box> { + setup_logger().map_err(|e| Box::new(Error::from(e)))?; let config = read_config()?; - run(config).await?; + run(config).await.map_err(|e| Box::new(Error::from(e)))?; Ok(()) } diff --git a/src/provider.rs b/src/provider.rs index b610ecc..6ea98c3 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -1,12 +1,14 @@ -use anyhow::Result; use async_trait::async_trait; use crate::types::dns::Record; +pub mod error; pub mod hetzner; pub mod netcup; pub mod nitrado; +pub use error::ProviderError; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum Feature { GetRecords, @@ -70,12 +72,12 @@ pub trait Provider: Send + Sync { &self, reqwest: reqwest::Client, input: &GetRecordsInput, - ) -> Result> { + ) -> Result, ProviderError> { let get_all_records_input = GetAllRecordsInput::from(input); let records = self .get_all_records(reqwest, &get_all_records_input) .await?; - let records = records + let records: Vec = records .into_iter() .filter(|record| input.subdomains.contains(&record.domain.as_str())) .collect(); @@ -87,11 +89,23 @@ pub trait Provider: Send + Sync { &self, reqwest: reqwest::Client, input: &GetAllRecordsInput, - ) -> Result>; + ) -> Result, ProviderError>; - async fn add_record(&self, reqwest: reqwest::Client, record: &Record) -> Result<()>; - async fn update_record(&self, reqwest: reqwest::Client, record: &Record) -> Result<()>; - async fn delete_record(&self, reqwest: reqwest::Client, record: &Record) -> Result<()>; + async fn add_record( + &self, + reqwest: reqwest::Client, + record: &Record, + ) -> Result<(), ProviderError>; + async fn update_record( + &self, + reqwest: reqwest::Client, + record: &Record, + ) -> Result<(), ProviderError>; + async fn delete_record( + &self, + reqwest: reqwest::Client, + record: &Record, + ) -> Result<(), ProviderError>; } #[cfg(test)] @@ -119,19 +133,31 @@ mod tests { &self, _reqwest: reqwest::Client, _input: &GetAllRecordsInput, - ) -> Result> { + ) -> Result, ProviderError> { Ok(self.records.clone()) } - async fn add_record(&self, _reqwest: reqwest::Client, _record: &Record) -> Result<()> { + async fn add_record( + &self, + _reqwest: reqwest::Client, + _record: &Record, + ) -> Result<(), ProviderError> { unimplemented!() } - async fn update_record(&self, _reqwest: reqwest::Client, _record: &Record) -> Result<()> { + async fn update_record( + &self, + _reqwest: reqwest::Client, + _record: &Record, + ) -> Result<(), ProviderError> { unimplemented!() } - async fn delete_record(&self, _reqwest: reqwest::Client, _record: &Record) -> Result<()> { + async fn delete_record( + &self, + _reqwest: reqwest::Client, + _record: &Record, + ) -> Result<(), ProviderError> { unimplemented!() } } diff --git a/src/provider/error.rs b/src/provider/error.rs new file mode 100644 index 0000000..8a0906a --- /dev/null +++ b/src/provider/error.rs @@ -0,0 +1,109 @@ +use thiserror::Error; + +use crate::provider::{hetzner, netcup, nitrado}; + +/// Error type for all DNS provider operations. +/// +/// This enum wraps provider-specific errors and common failure cases, +/// providing a unified error type for the provider trait. +#[derive(Debug, Error)] +pub enum ProviderError { + /// Error from Hetzner DNS provider + #[error("Hetzner provider error: {0}")] + Hetzner(#[from] hetzner::Error), + + /// Error from Nitrado DNS provider + #[error("Nitrado provider error: {0}")] + Nitrado(#[from] nitrado::Error), + + /// Error from Netcup DNS provider + #[error("Netcup provider error: {0}")] + Netcup(#[from] netcup::Error), +} + +#[cfg(test)] +mod tests { + use super::*; + use lum_libs::serde_json; + + #[test] + fn test_hetzner_error_display() { + let json_err = serde_json::from_str::("invalid").unwrap_err(); + let err = hetzner::Error::Json(json_err); + let display = format!("{}", err); + assert!(display.contains("JSON parsing error")); + } + + #[test] + fn test_hetzner_domain_not_found_display() { + let err = hetzner::Error::DomainNotFound("example.com".to_string()); + let display = format!("{}", err); + assert_eq!(display, "Domain 'example.com' not found in Hetzner zones"); + } + + #[test] + fn test_nitrado_error_display() { + let json_err = serde_json::from_str::("invalid").unwrap_err(); + let err = nitrado::Error::Json(json_err); + let display = format!("{}", err); + assert!(display.contains("JSON parsing error")); + } + + #[test] + fn test_netcup_error_display() { + let json_err = serde_json::from_str::("invalid").unwrap_err(); + let err = netcup::Error::Json(json_err); + let display = format!("{}", err); + assert!(display.contains("JSON parsing error")); + } + + #[test] + fn test_netcup_domain_not_found_display() { + let err = netcup::Error::DomainNotFound("example.com".to_string()); + let display = format!("{}", err); + assert_eq!(display, "Domain 'example.com' not found in Netcup zones"); + } + + #[test] + fn test_provider_error_from_hetzner() { + let hetzner_err = hetzner::Error::DomainNotFound("example.com".to_string()); + let provider_err: ProviderError = hetzner_err.into(); + + assert!(matches!(provider_err, ProviderError::Hetzner(_))); + let display = format!("{}", provider_err); + assert!(display.contains("Hetzner provider error")); + assert!(display.contains("example.com")); + } + + #[test] + fn test_provider_error_from_nitrado() { + let json_err = serde_json::from_str::("invalid").unwrap_err(); + let nitrado_err = nitrado::Error::Json(json_err); + let provider_err: ProviderError = nitrado_err.into(); + + assert!(matches!(provider_err, ProviderError::Nitrado(_))); + let display = format!("{}", provider_err); + assert!(display.contains("Nitrado provider error")); + } + + #[test] + fn test_provider_error_from_netcup() { + let netcup_err = netcup::Error::DomainNotFound("test.org".to_string()); + let provider_err: ProviderError = netcup_err.into(); + + assert!(matches!(provider_err, ProviderError::Netcup(_))); + let display = format!("{}", provider_err); + assert!(display.contains("Netcup provider error")); + assert!(display.contains("test.org")); + } + + #[test] + fn test_provider_error_debug() { + let hetzner_err = hetzner::Error::DomainNotFound("debug-test.com".to_string()); + let provider_err: ProviderError = hetzner_err.into(); + + let debug = format!("{:?}", provider_err); + assert!(debug.contains("Hetzner")); + assert!(debug.contains("DomainNotFound")); + } +} diff --git a/src/provider/hetzner.rs b/src/provider/hetzner.rs index a453327..2e1fe5a 100644 --- a/src/provider/hetzner.rs +++ b/src/provider/hetzner.rs @@ -1,11 +1,10 @@ -use anyhow::Result; use async_trait::async_trait; use lum_libs::serde_json; use reqwest::header::HeaderMap; use thiserror::Error; use crate::{ - provider::{Feature, GetAllRecordsInput, Provider}, + provider::{Feature, GetAllRecordsInput, Provider, ProviderError}, types::dns::{self}, }; @@ -24,18 +23,20 @@ impl<'provider_config> HetznerProvider<'provider_config> { HetznerProvider { provider_config } } - async fn get_zone_id(&self, reqwest: reqwest::Client, domain: &str) -> Result { + async fn get_zone_id(&self, reqwest: reqwest::Client, domain: &str) -> Result { let mut headers = HeaderMap::new(); headers.insert( "Auth-API-Token", - self.provider_config.api_key.parse().expect("Invalid Hetzner API key: contains characters that are not allowed in HTTP headers"), + self.provider_config.api_key.parse().expect( + "Invalid Hetzner API key: contains characters that are not allowed in HTTP headers", + ), ); let url = format!("{}/zones", self.provider_config.api_base_url); let response = reqwest.get(&url).headers(headers).send().await?; if !response.status().is_success() { - return Err(Error::Unsuccessful(response.status().as_u16(), response).into()); + return Err(Error::Unsuccessful(response.status().as_u16(), response)); } let text = response.text().await?; @@ -56,7 +57,7 @@ impl<'provider_config> HetznerProvider<'provider_config> { }) }) { Some(zone_id) => Ok(zone_id), - None => Err(Error::DomainNotFound(domain.to_string()).into()), + None => Err(Error::DomainNotFound(domain.to_string())), } } } @@ -74,6 +75,9 @@ pub enum Error { #[error("Domain '{0}' not found in Hetzner zones")] DomainNotFound(String), + + #[error("Record conversion error: {0}")] + RecordConversion(#[from] TryFromRecordError), } #[async_trait] @@ -92,16 +96,17 @@ impl Provider for HetznerProvider<'_> { ] } - async fn get_all_records( &self, reqwest: reqwest::Client, input: &GetAllRecordsInput, - ) -> Result> { + ) -> Result, ProviderError> { let mut headers = HeaderMap::new(); headers.insert( "Auth-API-Token", - self.provider_config.api_key.parse().expect("Invalid Hetzner API key: contains characters that are not allowed in HTTP headers"), + self.provider_config.api_key.parse().expect( + "Invalid Hetzner API key: contains characters that are not allowed in HTTP headers", + ), ); let domain = &input.domain; @@ -112,28 +117,45 @@ impl Provider for HetznerProvider<'_> { self.provider_config.api_base_url, zone_id ); - let response = reqwest.get(&url).headers(headers).send().await?; + let response = reqwest + .get(&url) + .headers(headers) + .send() + .await + .map_err(Error::Reqwest)?; if !response.status().is_success() { return Err(Error::Unsuccessful(response.status().as_u16(), response).into()); } - let text = response.text().await?; - let response: GetRecordsResponse = serde_json::from_str(&text)?; - let records: Vec = response.try_into()?; + let text = response.text().await.map_err(Error::Reqwest)?; + let response: GetRecordsResponse = serde_json::from_str(&text).map_err(Error::Json)?; + let records: Vec = response.try_into().map_err(Error::RecordConversion)?; Ok(records) } - async fn add_record(&self, _reqwest: reqwest::Client, _input: &dns::Record) -> Result<()> { + async fn add_record( + &self, + _reqwest: reqwest::Client, + _input: &dns::Record, + ) -> Result<(), ProviderError> { unimplemented!("Hetzner add_record not yet implemented") } - async fn update_record(&self, _reqwest: reqwest::Client, _input: &dns::Record) -> Result<()> { + async fn update_record( + &self, + _reqwest: reqwest::Client, + _input: &dns::Record, + ) -> Result<(), ProviderError> { unimplemented!("Hetzner update_record not yet implemented") } - async fn delete_record(&self, _reqwest: reqwest::Client, _input: &dns::Record) -> Result<()> { + async fn delete_record( + &self, + _reqwest: reqwest::Client, + _input: &dns::Record, + ) -> Result<(), ProviderError> { unimplemented!("Hetzner delete_record not yet implemented") } } diff --git a/src/provider/netcup.rs b/src/provider/netcup.rs index 247a2e4..7748263 100644 --- a/src/provider/netcup.rs +++ b/src/provider/netcup.rs @@ -1,10 +1,9 @@ -use anyhow::Result; use async_trait::async_trait; use lum_libs::serde_json; use thiserror::Error; use crate::{ - provider::{Feature, GetAllRecordsInput, Provider}, + provider::{Feature, GetAllRecordsInput, Provider, ProviderError}, types::dns::{self}, }; @@ -37,6 +36,9 @@ pub enum Error { #[error("Domain '{0}' not found in Netcup zones")] DomainNotFound(String), + + #[error("Record conversion error: {0}")] + RecordConversion(#[from] TryFromRecordError), } #[async_trait] @@ -55,24 +57,35 @@ impl Provider for NetcupProvider<'_> { ] } - async fn get_all_records( &self, _reqwest: reqwest::Client, _input: &GetAllRecordsInput, - ) -> Result> { + ) -> Result, ProviderError> { unimplemented!("Netcup get_all_records not yet implemented") } - async fn add_record(&self, _reqwest: reqwest::Client, _input: &dns::Record) -> Result<()> { + async fn add_record( + &self, + _reqwest: reqwest::Client, + _input: &dns::Record, + ) -> Result<(), ProviderError> { unimplemented!("Netcup add_record not yet implemented") } - async fn update_record(&self, _reqwest: reqwest::Client, _input: &dns::Record) -> Result<()> { + async fn update_record( + &self, + _reqwest: reqwest::Client, + _input: &dns::Record, + ) -> Result<(), ProviderError> { unimplemented!("Netcup update_record not yet implemented") } - async fn delete_record(&self, _reqwest: reqwest::Client, _input: &dns::Record) -> Result<()> { + async fn delete_record( + &self, + _reqwest: reqwest::Client, + _input: &dns::Record, + ) -> Result<(), ProviderError> { unimplemented!("Netcup delete_record not yet implemented") } } diff --git a/src/provider/nitrado.rs b/src/provider/nitrado.rs index e0adf64..f767a54 100644 --- a/src/provider/nitrado.rs +++ b/src/provider/nitrado.rs @@ -1,11 +1,10 @@ -use anyhow::Result; use async_trait::async_trait; use lum_libs::serde_json; use reqwest::header::HeaderMap; use thiserror::Error; use crate::{ - provider::{Feature, GetAllRecordsInput, Provider}, + provider::{Feature, GetAllRecordsInput, Provider, ProviderError}, types::dns::{self}, }; @@ -35,6 +34,9 @@ pub enum Error { #[error("JSON parsing error: {0}")] Json(#[from] serde_json::Error), + + #[error("Record conversion error: {0}")] + RecordConversion(#[from] TryFromRecordError), } #[async_trait] @@ -57,7 +59,7 @@ impl Provider for NitradoProvider<'_> { &self, reqwest: reqwest::Client, input: &GetAllRecordsInput, - ) -> Result> { + ) -> Result, ProviderError> { let mut headers = HeaderMap::new(); headers.insert( "Authorization", @@ -71,28 +73,45 @@ impl Provider for NitradoProvider<'_> { "{}/domain/{}/records", self.provider_config.api_base_url, domain ); - let response = reqwest.get(&url).headers(headers).send().await?; + let response = reqwest + .get(&url) + .headers(headers) + .send() + .await + .map_err(Error::Reqwest)?; if !response.status().is_success() { return Err(Error::Unsuccessful(response.status().as_u16(), response).into()); } - let text = response.text().await?; - let response: GetRecordsResponse = serde_json::from_str(&text)?; - let records: Vec = response.try_into()?; + let text = response.text().await.map_err(Error::Reqwest)?; + let response: GetRecordsResponse = serde_json::from_str(&text).map_err(Error::Json)?; + let records: Vec = response.try_into().map_err(Error::RecordConversion)?; Ok(records) } - async fn add_record(&self, _reqwest: reqwest::Client, _input: &dns::Record) -> Result<()> { + async fn add_record( + &self, + _reqwest: reqwest::Client, + _input: &dns::Record, + ) -> Result<(), ProviderError> { unimplemented!() } - async fn update_record(&self, _reqwest: reqwest::Client, _input: &dns::Record) -> Result<()> { + async fn update_record( + &self, + _reqwest: reqwest::Client, + _input: &dns::Record, + ) -> Result<(), ProviderError> { unimplemented!() } - async fn delete_record(&self, _reqwest: reqwest::Client, _input: &dns::Record) -> Result<()> { + async fn delete_record( + &self, + _reqwest: reqwest::Client, + _input: &dns::Record, + ) -> Result<(), ProviderError> { unimplemented!() } }