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#[derive(Debug, Clone, serde::Serialize)]
12pub struct MutationResponse {
13 pub status: u16,
14 pub location: Option<String>,
15}
16
17#[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 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}