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>(
47        &self,
48        path: &str,
49        query: &[(&str, &str)],
50    ) -> Result<T> {
51        self.request(
52            &self.config.trader_base_url,
53            Method::GET,
54            path,
55            query,
56            None::<&Value>,
57        )
58        .await
59    }
60
61    /// GET against Market Data Production (`/marketdata/v1`).
62    pub async fn get_market_data_json<T: DeserializeOwned>(
63        &self,
64        path: &str,
65        query: &[(&str, &str)],
66    ) -> Result<T> {
67        self.request(
68            crate::MARKET_DATA_BASE_URL,
69            Method::GET,
70            path,
71            query,
72            None::<&Value>,
73        )
74        .await
75    }
76
77    pub async fn post_json<T: DeserializeOwned>(&self, path: &str, body: &Value) -> Result<T> {
78        self.request(
79            &self.config.trader_base_url,
80            Method::POST,
81            path,
82            &[],
83            Some(body),
84        )
85        .await
86    }
87
88    pub async fn put_json<T: DeserializeOwned>(&self, path: &str, body: &Value) -> Result<T> {
89        self.request(
90            &self.config.trader_base_url,
91            Method::PUT,
92            path,
93            &[],
94            Some(body),
95        )
96        .await
97    }
98
99    pub async fn post_mutate(&self, path: &str, body: &Value) -> Result<MutationResponse> {
100        self.mutate(Method::POST, path, Some(body)).await
101    }
102
103    pub async fn put_mutate(&self, path: &str, body: &Value) -> Result<MutationResponse> {
104        self.mutate(Method::PUT, path, Some(body)).await
105    }
106
107    pub async fn delete_mutate(&self, path: &str) -> Result<MutationResponse> {
108        self.mutate(Method::DELETE, path, None).await
109    }
110
111    async fn mutate(
112        &self,
113        method: Method,
114        path: &str,
115        body: Option<&Value>,
116    ) -> Result<MutationResponse> {
117        let token = self.oauth.ensure_access_token().await?;
118        let url = format!("{}{}", self.config.trader_base_url, path);
119        debug!(%url, ?method, "API mutation");
120
121        let mut req = self
122            .http
123            .request(method, &url)
124            .bearer_auth(token)
125            .header("Accept", "application/json");
126
127        if let Some(body) = body {
128            req = req.json(body);
129        }
130
131        let response = req.send().await?;
132        let response = Self::ensure_success(response).await?;
133        Ok(Self::mutation_response(response))
134    }
135
136    async fn request<T: DeserializeOwned>(
137        &self,
138        base_url: &str,
139        method: Method,
140        path: &str,
141        query: &[(&str, &str)],
142        body: Option<&Value>,
143    ) -> Result<T> {
144        let token = self.oauth.ensure_access_token().await?;
145        let url = format!("{base_url}{path}");
146        debug!(%url, ?method, "API request");
147
148        let mut req = self
149            .http
150            .request(method, &url)
151            .bearer_auth(token)
152            .header("Accept", "application/json");
153
154        if !query.is_empty() {
155            req = req.query(query);
156        }
157        if let Some(body) = body {
158            req = req.json(body);
159        }
160
161        let response = req.send().await?;
162        let response = Self::ensure_success(response).await?;
163
164        if response.status() == reqwest::StatusCode::NO_CONTENT {
165            return Err(ApiError::Other("Unexpected empty response body".into()));
166        }
167
168        let bytes = response.bytes().await?;
169        if bytes.is_empty() {
170            return Err(ApiError::Other("Unexpected empty response body".into()));
171        }
172
173        Ok(serde_json::from_slice(&bytes)?)
174    }
175
176    fn mutation_response(response: Response) -> MutationResponse {
177        let status = response.status().as_u16();
178        let location = response
179            .headers()
180            .get("Location")
181            .and_then(|v| v.to_str().ok())
182            .map(str::to_string);
183        MutationResponse { status, location }
184    }
185
186    async fn ensure_success(response: Response) -> Result<Response> {
187        let status = response.status();
188        if status.is_success() {
189            return Ok(response);
190        }
191        let message = response.text().await.unwrap_or_default();
192        Err(ApiError::Api {
193            status: status.as_u16(),
194            message,
195        })
196    }
197}