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>(
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 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}