abstractapi/
lib.rs

1//! Rust API bindings for the Abstract HTTP API.
2
3#![warn(missing_docs)]
4
5/// API bindings.
6pub mod api;
7/// Error implementation.
8pub mod error;
9/// Common types that can be glob-imported for convenience.
10pub mod prelude;
11
12use api::*;
13use dashmap::DashMap;
14use error::{Error, Result};
15use std::fmt;
16use std::time::Duration;
17use ureq::{Agent as HttpClient, AgentBuilder, Request};
18
19/// Base domain for Abstract API.
20const ABSTRACTAPI_DOMAIN: &str = "abstractapi.com";
21
22/// A supported API which is in free/paid plan.
23#[derive(Debug, Eq, Hash, PartialEq)]
24pub enum ApiType {
25    /// Geolocation API.
26    Geolocation,
27    /// Holidays API.
28    Holidays,
29    /// Exchange rates API.
30    ExchangeRates,
31    /// Company details API.
32    CompanyEnrichment,
33    /// Timezone API.
34    Timezone,
35    /// Email validation API.
36    EmailValidation,
37    /// Phone validation API.
38    PhoneValidation,
39    /// VAT API.
40    Vat,
41}
42
43impl fmt::Display for ApiType {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(
46            f,
47            "{}",
48            match self {
49                Self::Geolocation => "ipgeolocation",
50                Self::Holidays => "holidays",
51                Self::ExchangeRates => "exchange-rates",
52                Self::CompanyEnrichment => "companyenrichment",
53                Self::Timezone => "timezone",
54                Self::EmailValidation => "emailvalidation",
55                Self::PhoneValidation => "phonevalidation",
56                Self::Vat => "vat",
57            }
58        )
59    }
60}
61
62/// Client for Abstract API.
63pub struct AbstractApi {
64    http_client: HttpClient,
65    api_keys: DashMap<ApiType, String>,
66}
67
68impl Default for AbstractApi {
69    fn default() -> Self {
70        Self::new_with_http_client(AgentBuilder::new().timeout(Duration::from_secs(15)).build())
71    }
72}
73
74impl AbstractApi {
75    /// Creates a new Abstract API client with the default HTTP client.
76    pub fn new<S: Into<String>>(
77        http_client: HttpClient,
78        api_keys: Vec<(ApiType, S)>,
79    ) -> Result<Self> {
80        let mut abstractapi = Self::new_with_http_client(http_client);
81        abstractapi.set_api_keys(api_keys)?;
82        Ok(abstractapi)
83    }
84
85    /// Creates a new Abstract API client that uses the given HTTP client.
86    pub fn new_with_http_client(http_client: HttpClient) -> Self {
87        Self {
88            http_client,
89            api_keys: DashMap::new(),
90        }
91    }
92
93    /// Creates a new Abstract API client with the given API key set.
94    pub fn new_with_api_key<S: Into<String>>(api_type: ApiType, api_key: S) -> Result<Self> {
95        let mut abstractapi = Self::default();
96        abstractapi.set_api_key(api_type, api_key)?;
97        Ok(abstractapi)
98    }
99
100    /// Creates a new Abstract API client with the given API keys set.
101    pub fn new_with_api_keys<S: Into<String>>(api_keys: Vec<(ApiType, S)>) -> Result<Self> {
102        let mut abstractapi = Self::default();
103        abstractapi.set_api_keys(api_keys)?;
104        Ok(abstractapi)
105    }
106
107    /// Sets an API key for an API.
108    pub fn set_api_key<S: Into<String>>(&mut self, api_type: ApiType, api_key: S) -> Result<()> {
109        match self.api_keys.insert(api_type, api_key.into()) {
110            Some(_) => Err(Error::ApiKeySetError),
111            _ => Ok(()),
112        }
113    }
114
115    /// Sets the API keys for specified APIs.
116    pub fn set_api_keys<S: Into<String>>(&mut self, api_keys: Vec<(ApiType, S)>) -> Result<()> {
117        for (api_type, api_key) in api_keys {
118            self.set_api_key(api_type, api_key)?;
119        }
120        Ok(())
121    }
122
123    /// Constructs and returns an HTTP request for an API.
124    fn get_api_request(&self, api_type: ApiType, path: &str) -> Result<Request> {
125        Ok(self
126            .http_client
127            .get(&format!(
128                "https://{}.{}/{}/",
129                api_type, ABSTRACTAPI_DOMAIN, path
130            ))
131            .query(
132                "api_key",
133                self.api_keys
134                    .get(&api_type)
135                    .ok_or(Error::ApiKeyNotPresent(api_type))?
136                    .value(),
137            ))
138    }
139
140    /// Upstream documentation: <https://app.abstractapi.com/api/ip-geolocation/documentation>
141    pub fn get_geolocation<S: AsRef<str>>(&self, ip_address: S) -> Result<Geolocation> {
142        Ok(self
143            .get_api_request(ApiType::Geolocation, "v1")?
144            .query("ip_address", ip_address.as_ref())
145            .call()
146            .map_err(Error::from)?
147            .into_json()?)
148    }
149
150    /// Upstream documentation: <https://app.abstractapi.com/api/holidays/documentation>
151    pub fn get_holidays<S: AsRef<str>>(
152        &self,
153        country: S,
154        year: S,
155        month: S,
156        day: S,
157    ) -> Result<Holidays> {
158        Ok(self
159            .get_api_request(ApiType::Holidays, "v1")?
160            .query("country", country.as_ref())
161            .query("year", year.as_ref())
162            .query("month", month.as_ref())
163            .query("day", day.as_ref())
164            .call()
165            .map_err(Error::from)?
166            .into_json()?)
167    }
168
169    /// Upstream documentation: <https://app.abstractapi.com/api/exchange-rates/documentation>
170    pub fn get_latest_exchange_rates<S: AsRef<str>>(
171        &self,
172        base: S,
173        target: Option<S>,
174    ) -> Result<ExchangeRatesResult> {
175        let mut request = self
176            .get_api_request(ApiType::ExchangeRates, "v1/live")?
177            .query("base", base.as_ref());
178        if let Some(target) = target {
179            request = request.query("target", target.as_ref());
180        }
181        Ok(request.call().map_err(Error::from)?.into_json()?)
182    }
183
184    /// Upstream documentation: <https://app.abstractapi.com/api/exchange-rates/documentation>
185    pub fn get_historical_exchange_rates<S: AsRef<str>>(
186        &self,
187        base: S,
188        target: Option<S>,
189        date: S,
190    ) -> Result<ExchangeRatesResult> {
191        let mut request = self
192            .get_api_request(ApiType::ExchangeRates, "v1/historical")?
193            .query("base", base.as_ref())
194            .query("date", date.as_ref());
195        if let Some(target) = target {
196            request = request.query("target", target.as_ref());
197        }
198        Ok(request.call().map_err(Error::from)?.into_json()?)
199    }
200
201    /// Upstream documentation: <https://app.abstractapi.com/api/exchange-rates/documentation>
202    pub fn convert_currency<S: AsRef<str>>(
203        &self,
204        base: S,
205        target: S,
206        date: Option<S>,
207        base_amount: Option<u64>,
208    ) -> Result<ConvertedExchangeRate> {
209        let mut request = self
210            .get_api_request(ApiType::ExchangeRates, "v1/convert")?
211            .query("base", base.as_ref())
212            .query("target", target.as_ref());
213        if let Some(date) = date {
214            request = request.query("date", date.as_ref());
215        }
216        if let Some(base_amount) = base_amount {
217            request = request.query("base_amount", &base_amount.to_string());
218        }
219        Ok(request.call().map_err(Error::from)?.into_json()?)
220    }
221
222    /// Upstream documentation: <https://app.abstractapi.com/api/company-enrichment/documentation>
223    pub fn get_company_details<S: AsRef<str>>(
224        &self,
225        domain: Option<S>,
226        email: Option<S>,
227    ) -> Result<CompanyDetails> {
228        let mut request = self.get_api_request(ApiType::CompanyEnrichment, "v1")?;
229        if let Some(domain) = domain {
230            request = request.query("domain", domain.as_ref());
231        }
232        if let Some(email) = email {
233            request = request.query("email", email.as_ref());
234        }
235        Ok(request.call().map_err(Error::from)?.into_json()?)
236    }
237
238    /// Upstream documentation: <https://app.abstractapi.com/api/timezone/documentation>
239    pub fn get_current_time<S: AsRef<str>>(&self, location: S) -> Result<LocationTime> {
240        Ok(self
241            .get_api_request(ApiType::Timezone, "v1/current_time")?
242            .query("location", location.as_ref())
243            .call()
244            .map_err(Error::from)?
245            .into_json()?)
246    }
247
248    /// Upstream documentation: <https://app.abstractapi.com/api/timezone/documentation>
249    pub fn convert_time<S: AsRef<str>>(
250        &self,
251        base_location: S,
252        base_datetime: S,
253        target_location: S,
254    ) -> Result<ConvertedTime> {
255        Ok(self
256            .get_api_request(ApiType::Timezone, "v1/convert_time")?
257            .query("base_location", base_location.as_ref())
258            .query("base_datetime", base_datetime.as_ref())
259            .query("target_location", target_location.as_ref())
260            .call()
261            .map_err(Error::from)?
262            .into_json()?)
263    }
264
265    /// Upstream documentation: <https://app.abstractapi.com/api/email-validation/documentation>
266    pub fn validate_email<S: AsRef<str>>(
267        &self,
268        email: S,
269        auto_correct: bool,
270    ) -> Result<EmailDetails> {
271        Ok(self
272            .get_api_request(ApiType::EmailValidation, "v1")?
273            .query("email", email.as_ref())
274            .query("auto_correct", &auto_correct.to_string())
275            .call()
276            .map_err(Error::from)?
277            .into_json()?)
278    }
279
280    /// Upstream documentation: <https://app.abstractapi.com/api/phone-validation/documentation>
281    pub fn validate_phone<S: AsRef<str>>(&self, phone: S) -> Result<PhoneDetails> {
282        Ok(self
283            .get_api_request(ApiType::PhoneValidation, "v1")?
284            .query("phone", phone.as_ref())
285            .call()
286            .map_err(Error::from)?
287            .into_json()?)
288    }
289
290    /// Upstream documentation: <https://app.abstractapi.com/api/vat/documentation>
291    pub fn validate_vat<S: AsRef<str>>(&self, vat_number: S) -> Result<VatDetails> {
292        Ok(self
293            .get_api_request(ApiType::Vat, "v1/validate")?
294            .query("vat_number", vat_number.as_ref())
295            .call()
296            .map_err(Error::from)?
297            .into_json()?)
298    }
299
300    /// Upstream documentation: <https://app.abstractapi.com/api/vat/documentation>
301    pub fn calculate_vat<S: AsRef<str>>(
302        &self,
303        amount: f64,
304        country_code: S,
305        is_vat_incl: bool,
306        vat_category: Option<S>,
307    ) -> Result<Vat> {
308        let mut request = self
309            .get_api_request(ApiType::Vat, "v1/calculate")?
310            .query("amount", &amount.to_string())
311            .query("country_code", country_code.as_ref())
312            .query("is_vat_incl", &is_vat_incl.to_string());
313        if let Some(vat_category) = vat_category {
314            request = request.query("vat_category", vat_category.as_ref())
315        }
316        Ok(request.call().map_err(Error::from)?.into_json()?)
317    }
318
319    /// Upstream documentation: <https://app.abstractapi.com/api/vat/documentation>
320    pub fn get_vat_rates<S: AsRef<str>>(&self, country_code: S) -> Result<VatRates> {
321        Ok(self
322            .get_api_request(ApiType::Vat, "v1/categories")?
323            .query("country_code", country_code.as_ref())
324            .call()
325            .map_err(Error::from)?
326            .into_json()?)
327    }
328}