1#![warn(missing_docs)]
52
53mod api;
54pub mod entity;
55pub mod error;
56pub use api::*;
57pub use entity::*;
58
59use crate::error::{
60 DataForSeoApiError, DataForSeoError, DataForSeoHttpError, DataForSeoSystemError,
61};
62use base64::Engine;
63use base64::prelude::BASE64_STANDARD;
64use serde_json::Value;
65
66pub const DEFAULT_BASE_URL: &str = "https://api.dataforseo.com";
68pub const SANDBOX_BASE_URL: &str = "https://sandbox.dataforseo.com";
71
72#[derive(Debug, Clone)]
77pub struct DataForSeoClient {
78 http: reqwest::Client,
79 base_url: String,
80 token: String,
81}
82
83impl DataForSeoClient {
84 pub fn new<L: Into<String>, P: Into<String>>(login: L, password: P) -> DataForSeoClient {
86 DataForSeoClient {
87 http: reqwest::Client::new(),
88 base_url: DEFAULT_BASE_URL.to_string(),
89 token: BASE64_STANDARD.encode(format!("{}:{}", login.into(), password.into())),
90 }
91 }
92
93 pub fn from_env() -> Result<DataForSeoClient, DataForSeoError> {
98 let login = std::env::var("DATAFORSEO_LOGIN")
99 .map_err(|_| DataForSeoSystemError::new("DATAFORSEO_LOGIN is not set"))?;
100 let password = std::env::var("DATAFORSEO_PASSWORD")
101 .map_err(|_| DataForSeoSystemError::new("DATAFORSEO_PASSWORD is not set"))?;
102 Ok(DataForSeoClient::new(login, password))
103 }
104
105 pub fn with_base_url<T: Into<String>>(mut self, base_url: T) -> Self {
107 self.base_url = base_url.into().trim_end_matches('/').to_string();
108 self
109 }
110
111 pub fn sandbox(self) -> Self {
113 self.with_base_url(SANDBOX_BASE_URL)
114 }
115
116 pub fn with_http_client(mut self, http: reqwest::Client) -> Self {
118 self.http = http;
119 self
120 }
121}
122
123pub type DataForSeoApiResponse<R> = Result<entity::DataForSeoApiResponseData<R>, DataForSeoError>;
125
126impl DataForSeoClient {
127 fn endpoint(&self, path: &str) -> String {
128 format!("{}{}", self.base_url, path)
129 }
130
131 async fn read_response<R>(response: reqwest::Response) -> DataForSeoApiResponse<R>
132 where
133 R: for<'de> serde::Deserialize<'de>,
134 {
135 let status = response.status().as_u16();
136 let body = match response.text().await {
137 Ok(v) => v,
138 Err(e) => return Err(DataForSeoHttpError::new(status, e.to_string()).into()),
139 };
140
141 let value = match serde_json::from_str::<Value>(&body) {
142 Ok(v) => v,
143 Err(_) => return Err(DataForSeoHttpError::new(status, body).into()),
144 };
145
146 if status >= 400 {
147 return Err(DataForSeoApiError::from_envelope(status, &value, body).into());
148 }
149
150 match serde_json::from_value::<entity::DataForSeoApiResponseData<R>>(value.clone()) {
151 Ok(v) => Ok(v),
152 Err(e) => Err(DataForSeoSystemError::new(format!(
153 "failed to deserialize response: {e} body={value}"
154 ))
155 .into()),
156 }
157 }
158
159 pub(crate) async fn http_get<R>(&self, path: &str) -> DataForSeoApiResponse<R>
160 where
161 R: for<'de> serde::Deserialize<'de>,
162 {
163 let request = self
164 .http
165 .get(self.endpoint(path))
166 .header("Authorization", format!("Basic {}", self.token));
167 match request.send().await {
168 Ok(response) => Self::read_response(response).await,
169 Err(e) => Err(DataForSeoSystemError::new(format!("request failed: {e}")).into()),
170 }
171 }
172
173 pub(crate) async fn http_post<P, R>(&self, path: &str, body: &P) -> DataForSeoApiResponse<R>
174 where
175 P: serde::Serialize,
176 R: for<'de> serde::Deserialize<'de>,
177 {
178 let request = self
179 .http
180 .post(self.endpoint(path))
181 .header("Authorization", format!("Basic {}", self.token))
182 .json(body);
183 match request.send().await {
184 Ok(response) => Self::read_response(response).await,
185 Err(e) => Err(DataForSeoSystemError::new(format!("request failed: {e}")).into()),
186 }
187 }
188}