Skip to main content

auto_api_client/
client.rs

1use std::collections::HashMap;
2use std::time::Duration;
3
4use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
5use serde_json::Value;
6
7use crate::error::Error;
8use crate::types::*;
9
10/// Client for the auto-api.com car listings API.
11pub struct Client {
12    api_key: String,
13    base_url: String,
14    api_version: String,
15    http_client: reqwest::Client,
16}
17
18impl Client {
19    /// Creates a new API client with the given API key.
20    pub fn new(api_key: &str) -> Self {
21        Self {
22            api_key: api_key.to_string(),
23            base_url: "https://auto-api.com".to_string(),
24            api_version: "v2".to_string(),
25            http_client: reqwest::Client::builder()
26                .timeout(Duration::from_secs(30))
27                .build()
28                .expect("failed to build HTTP client"),
29        }
30    }
31
32    /// Sets a custom base URL.
33    pub fn set_base_url(&mut self, base_url: &str) {
34        self.base_url = base_url.trim_end_matches('/').to_string();
35    }
36
37    /// Sets a custom API version (default: "v2").
38    pub fn set_api_version(&mut self, version: &str) {
39        self.api_version = version.to_string();
40    }
41
42    /// Returns available filters for a source (brands, models, body types, etc.)
43    pub async fn get_filters(&self, source: &str) -> Result<Value, Error> {
44        let url = format!(
45            "{}/api/{}/{}/filters",
46            self.base_url, self.api_version, source
47        );
48        self.get(&url, &[]).await
49    }
50
51    /// Returns a paginated list of offers with optional filters.
52    pub async fn get_offers(
53        &self,
54        source: &str,
55        params: &OffersParams,
56    ) -> Result<OffersResponse, Error> {
57        let url = format!(
58            "{}/api/{}/{}/offers",
59            self.base_url, self.api_version, source
60        );
61        let pairs = params.to_query_pairs();
62        let query: Vec<(&str, &str)> = pairs.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
63        self.get(&url, &query).await
64    }
65
66    /// Returns a single offer by inner_id.
67    pub async fn get_offer(
68        &self,
69        source: &str,
70        inner_id: &str,
71    ) -> Result<OffersResponse, Error> {
72        let url = format!(
73            "{}/api/{}/{}/offer",
74            self.base_url, self.api_version, source
75        );
76        self.get(&url, &[("inner_id", inner_id)]).await
77    }
78
79    /// Returns a change_id for the given date (format: yyyy-mm-dd).
80    pub async fn get_change_id(&self, source: &str, date: &str) -> Result<i64, Error> {
81        let url = format!(
82            "{}/api/{}/{}/change_id",
83            self.base_url, self.api_version, source
84        );
85        let result: ChangeIdResponse = self.get(&url, &[("date", date)]).await?;
86        Ok(result.change_id)
87    }
88
89    /// Returns a changes feed (added/changed/removed) starting from change_id.
90    pub async fn get_changes(
91        &self,
92        source: &str,
93        change_id: i64,
94    ) -> Result<ChangesResponse, Error> {
95        let url = format!(
96            "{}/api/{}/{}/changes",
97            self.base_url, self.api_version, source
98        );
99        let change_id_str = change_id.to_string();
100        self.get(&url, &[("change_id", &change_id_str)]).await
101    }
102
103    /// Returns offer data by its URL on the marketplace.
104    /// Uses POST /api/v1/offer/info with x-api-key header.
105    pub async fn get_offer_by_url(&self, offer_url: &str) -> Result<Value, Error> {
106        let url = format!("{}/api/v1/offer/info", self.base_url);
107
108        let mut body = HashMap::new();
109        body.insert("url", offer_url);
110
111        let mut headers = HeaderMap::new();
112        headers.insert("x-api-key", HeaderValue::from_str(&self.api_key).unwrap());
113        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
114
115        let response = self
116            .http_client
117            .post(&url)
118            .headers(headers)
119            .json(&body)
120            .send()
121            .await?;
122
123        self.handle_response(response).await
124    }
125
126    async fn get<T: serde::de::DeserializeOwned>(
127        &self,
128        url: &str,
129        query: &[(&str, &str)],
130    ) -> Result<T, Error> {
131        let mut all_query: Vec<(&str, &str)> = query.to_vec();
132        all_query.push(("api_key", &self.api_key));
133
134        let response = self
135            .http_client
136            .get(url)
137            .query(&all_query)
138            .send()
139            .await?;
140
141        self.handle_response(response).await
142    }
143
144    async fn handle_response<T: serde::de::DeserializeOwned>(
145        &self,
146        response: reqwest::Response,
147    ) -> Result<T, Error> {
148        let status = response.status().as_u16();
149        let body = response.text().await?;
150
151        if status < 200 || status >= 300 {
152            let mut message = format!("API error: {}", status);
153
154            if let Ok(parsed) = serde_json::from_str::<Value>(&body) {
155                if let Some(msg) = parsed.get("message").and_then(|m| m.as_str()) {
156                    message = msg.to_string();
157                }
158            }
159
160            if status == 401 || status == 403 {
161                return Err(Error::Auth {
162                    status_code: status,
163                    message,
164                });
165            }
166
167            return Err(Error::Api {
168                status_code: status,
169                message,
170                body,
171            });
172        }
173
174        serde_json::from_str(&body).map_err(|_| Error::Api {
175            status_code: status,
176            message: format!(
177                "Invalid JSON response: {}",
178                &body[..body.len().min(200)]
179            ),
180            body,
181        })
182    }
183}