#![warn(missing_docs)]
mod api;
pub mod entity;
pub mod error;
pub use api::*;
pub use entity::*;
use crate::error::{
DataForSeoApiError, DataForSeoError, DataForSeoHttpError, DataForSeoSystemError,
};
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use serde_json::Value;
pub const DEFAULT_BASE_URL: &str = "https://api.dataforseo.com";
pub const SANDBOX_BASE_URL: &str = "https://sandbox.dataforseo.com";
#[derive(Debug, Clone)]
pub struct DataForSeoClient {
http: reqwest::Client,
base_url: String,
token: String,
}
impl DataForSeoClient {
pub fn new<L: Into<String>, P: Into<String>>(login: L, password: P) -> DataForSeoClient {
DataForSeoClient {
http: reqwest::Client::new(),
base_url: DEFAULT_BASE_URL.to_string(),
token: BASE64_STANDARD.encode(format!("{}:{}", login.into(), password.into())),
}
}
pub fn from_env() -> Result<DataForSeoClient, DataForSeoError> {
let login = std::env::var("DATAFORSEO_LOGIN")
.map_err(|_| DataForSeoSystemError::new("DATAFORSEO_LOGIN is not set"))?;
let password = std::env::var("DATAFORSEO_PASSWORD")
.map_err(|_| DataForSeoSystemError::new("DATAFORSEO_PASSWORD is not set"))?;
Ok(DataForSeoClient::new(login, password))
}
pub fn with_base_url<T: Into<String>>(mut self, base_url: T) -> Self {
self.base_url = base_url.into().trim_end_matches('/').to_string();
self
}
pub fn sandbox(self) -> Self {
self.with_base_url(SANDBOX_BASE_URL)
}
pub fn with_http_client(mut self, http: reqwest::Client) -> Self {
self.http = http;
self
}
}
pub type DataForSeoApiResponse<R> = Result<entity::DataForSeoApiResponseData<R>, DataForSeoError>;
impl DataForSeoClient {
fn endpoint(&self, path: &str) -> String {
format!("{}{}", self.base_url, path)
}
async fn read_response<R>(response: reqwest::Response) -> DataForSeoApiResponse<R>
where
R: for<'de> serde::Deserialize<'de>,
{
let status = response.status().as_u16();
let body = match response.text().await {
Ok(v) => v,
Err(e) => return Err(DataForSeoHttpError::new(status, e.to_string()).into()),
};
let value = match serde_json::from_str::<Value>(&body) {
Ok(v) => v,
Err(_) => return Err(DataForSeoHttpError::new(status, body).into()),
};
if status >= 400 {
return Err(DataForSeoApiError::from_envelope(status, &value, body).into());
}
match serde_json::from_value::<entity::DataForSeoApiResponseData<R>>(value.clone()) {
Ok(v) => Ok(v),
Err(e) => Err(DataForSeoSystemError::new(format!(
"failed to deserialize response: {e} body={value}"
))
.into()),
}
}
pub(crate) async fn http_get<R>(&self, path: &str) -> DataForSeoApiResponse<R>
where
R: for<'de> serde::Deserialize<'de>,
{
let request = self
.http
.get(self.endpoint(path))
.header("Authorization", format!("Basic {}", self.token));
match request.send().await {
Ok(response) => Self::read_response(response).await,
Err(e) => Err(DataForSeoSystemError::new(format!("request failed: {e}")).into()),
}
}
pub(crate) async fn http_post<P, R>(&self, path: &str, body: &P) -> DataForSeoApiResponse<R>
where
P: serde::Serialize,
R: for<'de> serde::Deserialize<'de>,
{
let request = self
.http
.post(self.endpoint(path))
.header("Authorization", format!("Basic {}", self.token))
.json(body);
match request.send().await {
Ok(response) => Self::read_response(response).await,
Err(e) => Err(DataForSeoSystemError::new(format!("request failed: {e}")).into()),
}
}
}