Skip to main content

schwab_api/
client.rs

1use reqwest::{Client, Method, Response};
2use serde::de::DeserializeOwned;
3use serde_json::Value;
4use tracing::debug;
5
6use crate::auth::OAuthClient;
7use crate::config::ClientConfig;
8use crate::error::{ApiError, Result};
9
10/// Response for POST/PUT/DELETE calls that may return an empty body.
11#[derive(Debug, Clone, serde::Serialize)]
12pub struct MutationResponse {
13    pub status: u16,
14    pub location: Option<String>,
15}
16
17/// Authenticated HTTP client for Trader API v1.
18#[derive(Debug, Clone)]
19pub struct SchwabClient {
20    http: Client,
21    config: ClientConfig,
22    oauth: OAuthClient,
23}
24
25impl SchwabClient {
26    pub fn new(config: ClientConfig) -> Self {
27        let oauth = OAuthClient::new(config.clone());
28        Self {
29            http: Client::builder()
30                .gzip(true)
31                .build()
32                .expect("reqwest client"),
33            config,
34            oauth,
35        }
36    }
37
38    pub fn oauth(&self) -> &OAuthClient {
39        &self.oauth
40    }
41
42    pub fn config(&self) -> &ClientConfig {
43        &self.config
44    }
45
46    pub async fn get_json<T: DeserializeOwned>(&self, path: &str, query: &[(&str, &str)]) -> Result<T> {
47        self.request(
48            &self.config.trader_base_url,
49            Method::GET,
50            path,
51            query,
52            None::<&Value>,
53        )
54        .await
55    }
56
57    /// GET against Market Data Production (`/marketdata/v1`).
58    pub async fn get_market_data_json<T: DeserializeOwned>(
59        &self,
60        path: &str,
61        query: &[(&str, &str)],
62    ) -> Result<T> {
63        self.request(
64            crate::MARKET_DATA_BASE_URL,
65            Method::GET,
66            path,
67            query,
68            None::<&Value>,
69        )
70        .await
71    }
72
73    pub async fn post_json<T: DeserializeOwned>(&self, path: &str, body: &Value) -> Result<T> {
74        self.request(
75            &self.config.trader_base_url,
76            Method::POST,
77            path,
78            &[],
79            Some(body),
80        )
81        .await
82    }
83
84    pub async fn put_json<T: DeserializeOwned>(&self, path: &str, body: &Value) -> Result<T> {
85        self.request(
86            &self.config.trader_base_url,
87            Method::PUT,
88            path,
89            &[],
90            Some(body),
91        )
92        .await
93    }
94
95    pub async fn post_mutate(&self, path: &str, body: &Value) -> Result<MutationResponse> {
96        self.mutate(Method::POST, path, Some(body)).await
97    }
98
99    pub async fn put_mutate(&self, path: &str, body: &Value) -> Result<MutationResponse> {
100        self.mutate(Method::PUT, path, Some(body)).await
101    }
102
103    pub async fn delete_mutate(&self, path: &str) -> Result<MutationResponse> {
104        self.mutate(Method::DELETE, path, None).await
105    }
106
107    async fn mutate(
108        &self,
109        method: Method,
110        path: &str,
111        body: Option<&Value>,
112    ) -> Result<MutationResponse> {
113        let token = self.oauth.ensure_access_token().await?;
114        let url = format!("{}{}", self.config.trader_base_url, path);
115        debug!(%url, ?method, "API mutation");
116
117        let mut req = self
118            .http
119            .request(method, &url)
120            .bearer_auth(token)
121            .header("Accept", "application/json");
122
123        if let Some(body) = body {
124            req = req.json(body);
125        }
126
127        let response = req.send().await?;
128        let response = Self::ensure_success(response).await?;
129        Ok(Self::mutation_response(response))
130    }
131
132    async fn request<T: DeserializeOwned>(
133        &self,
134        base_url: &str,
135        method: Method,
136        path: &str,
137        query: &[(&str, &str)],
138        body: Option<&Value>,
139    ) -> Result<T> {
140        let token = self.oauth.ensure_access_token().await?;
141        let url = format!("{base_url}{path}");
142        debug!(%url, ?method, "API request");
143
144        let mut req = self
145            .http
146            .request(method, &url)
147            .bearer_auth(token)
148            .header("Accept", "application/json");
149
150        if !query.is_empty() {
151            req = req.query(query);
152        }
153        if let Some(body) = body {
154            req = req.json(body);
155        }
156
157        let response = req.send().await?;
158        let response = Self::ensure_success(response).await?;
159
160        if response.status() == reqwest::StatusCode::NO_CONTENT {
161            return Err(ApiError::Other("Unexpected empty response body".into()));
162        }
163
164        let bytes = response.bytes().await?;
165        if bytes.is_empty() {
166            return Err(ApiError::Other("Unexpected empty response body".into()));
167        }
168
169        Ok(serde_json::from_slice(&bytes)?)
170    }
171
172    fn mutation_response(response: Response) -> MutationResponse {
173        let status = response.status().as_u16();
174        let location = response
175            .headers()
176            .get("Location")
177            .and_then(|v| v.to_str().ok())
178            .map(str::to_string);
179        MutationResponse { status, location }
180    }
181
182    async fn ensure_success(response: Response) -> Result<Response> {
183        let status = response.status();
184        if status.is_success() {
185            return Ok(response);
186        }
187        let message = response.text().await.unwrap_or_default();
188        Err(ApiError::Api {
189            status: status.as_u16(),
190            message,
191        })
192    }
193}