1use chrono::{DateTime, Utc};
17use serde::{Deserialize, Serialize, de::DeserializeOwned};
18use serde_with::skip_serializing_none;
19use std::collections::HashMap;
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
23#[serde(rename_all = "camelCase")]
24#[non_exhaustive]
25pub struct ResponseHeaders {
26 #[serde(rename = "Schwab-Client-CorrelId")]
27 pub schwab_client_correlid: Option<String>,
28 #[serde(rename = "Schwab-Resource-Version")]
29 pub schwab_resource_version: Option<String>,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
39#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
40#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
41#[non_exhaustive]
42pub enum AssetType {
43 #[default]
44 Equity,
45 Option,
46 Index,
47 MutualFund,
48 CashEquivalent,
49 FixedIncome,
50 Currency,
51 CollectiveInvestment,
52 Future,
53 Forex,
54 Product,
55 #[serde(other)]
56 #[cfg_attr(feature = "schemars", schemars(skip))]
57 Unknown,
58}
59
60pub trait Wire {
68 fn is_unknown(&self) -> bool;
71}
72
73macro_rules! wire_enum {
75 ($($ty:ty),+ $(,)?) => {$(
76 impl $crate::common::Wire for $ty {
77 fn is_unknown(&self) -> bool {
78 matches!(self, Self::Unknown)
79 }
80 }
81 )+};
82}
83pub(crate) use wire_enum;
84
85wire_enum!(AssetType);
86
87pub(crate) fn known<T: Wire>(
89 field: &str,
90 value: Option<&T>,
91) -> Result<(), crate::validation::ValidationError> {
92 match value {
93 Some(v) if v.is_unknown() => Err(crate::validation::ValidationError::new(
94 crate::validation::ValidationErrorKind::Unsupported,
95 Some(field),
96 format!("{field} is not a value Schwab accepts"),
97 )),
98 _ => Ok(()),
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
109#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
110#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
111#[non_exhaustive]
112pub enum AssetMainType {
113 Bond,
114 Equity,
115 Etf,
116 Extended,
117 Forex,
118 Future,
119 FutureOption,
120 Fundamental,
121 Index,
122 Indicator,
123 MutualFund,
124 Option,
125 #[serde(other)]
126 Unknown,
127}
128
129#[skip_serializing_none]
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
136#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
137#[serde(rename_all = "camelCase", default)]
139#[non_exhaustive]
140pub struct AccountsInstrument {
141 pub asset_type: AssetType,
142 pub symbol: String,
143 pub cusip: Option<String>,
144 pub description: Option<String>,
145 pub instrument_id: Option<i64>,
146 pub net_change: Option<f64>,
147 #[serde(rename = "type")]
150 pub type_: Option<String>,
151 #[serde(default, deserialize_with = "lenient")]
153 pub put_call: Option<PutCall>,
154 pub underlying_symbol: Option<String>,
156 pub option_multiplier: Option<i64>,
158 pub maturity_date: Option<String>,
160 pub factor: Option<f64>,
162 pub variable_rate: Option<f64>,
164 #[serde(flatten)]
165 pub extra: HashMap<String, serde_json::Value>,
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
174#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
175#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
176#[non_exhaustive]
177pub enum AssetSubType {
178 Adr,
179 Cef,
180 Coe,
181 Etf,
182 Etn,
183 Gdr,
184 Mmf,
185 Oef,
186 Prf,
187 Rgt,
188 Uit,
189 War,
190 #[serde(other)]
191 Unknown,
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
196#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
197#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
198pub enum PutCall {
199 Put,
200 Call,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
204#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
205#[serde(rename_all = "camelCase")]
206#[non_exhaustive]
207pub struct PageInfo {
208 pub page_size: i32,
209 pub page_number: i32,
210 pub total_pages: i32,
211 pub total_items: i32,
212}
213
214pub fn normalize_integral_numbers(value: &mut serde_json::Value) {
219 use serde_json::Value;
220 const EXACT: f64 = 9_007_199_254_740_992.0;
221 match value {
222 Value::Number(number) if number.is_f64() => {
223 if let Some(float) = number.as_f64()
224 && float.fract() == 0.0
225 && float.abs() <= EXACT
226 {
227 *value = Value::from(float as i64);
228 }
229 }
230 Value::Array(items) => items.iter_mut().for_each(normalize_integral_numbers),
231 Value::Object(fields) => fields.values_mut().for_each(normalize_integral_numbers),
232 _ => {}
233 }
234}
235
236pub fn optional_id<'de, D: serde::Deserializer<'de>>(de: D) -> Result<Option<String>, D::Error> {
238 let value = Option::<serde_json::Value>::deserialize(de)?;
239 match value {
240 None => Ok(None),
241 Some(serde_json::Value::String(s)) => Ok(Some(s)),
242 Some(serde_json::Value::Number(n)) if n.is_u64() || n.is_i64() => Ok(Some(n.to_string())),
243 _ => Err(serde::de::Error::custom(
244 "Expected an integer or string identifier",
245 )),
246 }
247}
248
249pub fn lenient<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
254where
255 D: serde::Deserializer<'de>,
256 T: DeserializeOwned,
257{
258 let value = Option::<serde_json::Value>::deserialize(de)?;
259 Ok(value.and_then(|v| serde_json::from_value(v).ok()))
260}
261
262pub mod exact_decimal {
271 use rust_decimal::Decimal;
272 use serde::de::{self, Deserializer, Visitor};
273 use std::fmt;
274
275 pub fn parse(s: &str) -> Result<Decimal, String> {
276 let plain = !s.is_empty()
277 && s.bytes()
278 .all(|b| b.is_ascii_digit() || b == b'.' || b == b'-' || b == b'+');
279 if !plain {
280 return Err(format!("{s:?} is not a plain decimal number"));
281 }
282 Decimal::from_str_exact(s).map_err(|e| format!("{s:?} is not an exact decimal: {e}"))
283 }
284
285 struct Exact;
286
287 impl Visitor<'_> for Exact {
288 type Value = Decimal;
289
290 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 f.write_str("a decimal number as a string, such as \"150.25\"")
292 }
293
294 fn visit_str<E: de::Error>(self, s: &str) -> Result<Decimal, E> {
295 parse(s).map_err(E::custom)
296 }
297
298 fn visit_i64<E: de::Error>(self, n: i64) -> Result<Decimal, E> {
299 Ok(Decimal::from(n))
300 }
301
302 fn visit_u64<E: de::Error>(self, n: u64) -> Result<Decimal, E> {
303 Ok(Decimal::from(n))
304 }
305
306 fn visit_f64<E: de::Error>(self, n: f64) -> Result<Decimal, E> {
307 if !n.is_finite() {
308 return Err(E::custom("decimal must be finite"));
309 }
310 parse(&n.to_string()).map_err(E::custom)
312 }
313 }
314
315 pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Decimal, D::Error> {
316 de.deserialize_any(Exact)
317 }
318
319 pub fn serialize_number<S: serde::Serializer>(
324 value: &Decimal,
325 s: S,
326 ) -> Result<S::Ok, S::Error> {
327 use rust_decimal::prelude::ToPrimitive;
328 use serde::ser::Error;
329 if value.fract().is_zero()
330 && let Some(n) = value.to_i64()
331 {
332 return s.serialize_i64(n);
333 }
334 match value.to_f64() {
335 Some(n) if parse(&n.to_string()).is_ok_and(|back| back == *value) => s.serialize_f64(n),
336 _ => Err(S::Error::custom(format!(
337 "{value} has more precision than a JSON number carries"
338 ))),
339 }
340 }
341
342 pub fn serialize_string<S: serde::Serializer>(
347 value: &Decimal,
348 s: S,
349 ) -> Result<S::Ok, S::Error> {
350 s.collect_str(value)
351 }
352
353 pub mod option {
355 use rust_decimal::Decimal;
356 use serde::{Deserialize, Deserializer, Serializer};
357
358 pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Option<Decimal>, D::Error> {
359 #[derive(Deserialize)]
360 struct Wrap(#[serde(deserialize_with = "super::deserialize")] Decimal);
361 Ok(Option::<Wrap>::deserialize(de)?.map(|Wrap(d)| d))
362 }
363
364 pub fn serialize_number<S: Serializer>(
365 value: &Option<Decimal>,
366 s: S,
367 ) -> Result<S::Ok, S::Error> {
368 match value {
369 Some(value) => super::serialize_number(value, s),
370 None => s.serialize_none(),
371 }
372 }
373
374 pub fn serialize_string<S: Serializer>(
375 value: &Option<Decimal>,
376 s: S,
377 ) -> Result<S::Ok, S::Error> {
378 match value {
379 Some(value) => super::serialize_string(value, s),
380 None => s.serialize_none(),
381 }
382 }
383 }
384}
385
386#[cfg(feature = "schemars")]
389#[derive(Debug)]
390pub struct DecimalString;
391
392#[cfg(feature = "schemars")]
393impl schemars::JsonSchema for DecimalString {
394 fn schema_name() -> std::borrow::Cow<'static, str> {
395 "DecimalString".into()
396 }
397
398 fn inline_schema() -> bool {
399 true
400 }
401
402 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
403 schemars::json_schema!({
404 "type": "string",
405 "pattern": "^[0-9]+(\\.[0-9]+)?$",
406 "description": "Exact decimal as a string, e.g. \"150.25\"; never a JSON number."
407 })
408 }
409}
410
411#[cfg(feature = "schemars")]
414#[derive(Debug)]
415pub struct QuantityNumber;
416
417#[cfg(feature = "schemars")]
418impl schemars::JsonSchema for QuantityNumber {
419 fn schema_name() -> std::borrow::Cow<'static, str> {
420 "QuantityNumber".into()
421 }
422
423 fn inline_schema() -> bool {
424 true
425 }
426
427 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
428 schemars::json_schema!({
429 "type": ["number", "string"],
430 "pattern": "^[0-9]+(\\.[0-9]+)?$",
431 "description": "Shares, contracts or fund units: a JSON number such as 10, \
432 or a decimal string such as \"10\"."
433 })
434 }
435}
436
437pub fn schwab_timestamp(at: DateTime<Utc>) -> String {
453 at.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()
454}
455
456pub fn parse_schwab_time(text: &str) -> Result<DateTime<Utc>, chrono::ParseError> {
459 DateTime::parse_from_str(text, "%Y-%m-%dT%H:%M:%S%z")
460 .or_else(|_| DateTime::parse_from_str(text, "%Y-%m-%dT%H:%M:%S%.f%z"))
461 .or_else(|_| DateTime::parse_from_rfc3339(text))
462 .map(|at| at.with_timezone(&Utc))
463}
464
465pub(crate) fn serialize_timestamp<S: serde::Serializer>(
467 at: &DateTime<Utc>,
468 serializer: S,
469) -> Result<S::Ok, S::Error> {
470 serializer.serialize_str(&schwab_timestamp(*at))
471}
472
473pub(crate) fn serialize_optional_timestamp<S: serde::Serializer>(
475 at: &Option<DateTime<Utc>>,
476 serializer: S,
477) -> Result<S::Ok, S::Error> {
478 match at {
479 Some(at) => serialize_timestamp(at, serializer),
480 None => serializer.serialize_none(),
481 }
482}