use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_with::skip_serializing_none;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ResponseHeaders {
#[serde(rename = "Schwab-Client-CorrelId")]
pub schwab_client_correlid: Option<String>,
#[serde(rename = "Schwab-Resource-Version")]
pub schwab_resource_version: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum AssetType {
#[default]
Equity,
Option,
Index,
MutualFund,
CashEquivalent,
FixedIncome,
Currency,
CollectiveInvestment,
Future,
Forex,
Product,
#[serde(other)]
#[cfg_attr(feature = "schemars", schemars(skip))]
Unknown,
}
pub trait Wire {
fn is_unknown(&self) -> bool;
}
macro_rules! wire_enum {
($($ty:ty),+ $(,)?) => {$(
impl $crate::common::Wire for $ty {
fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown)
}
}
)+};
}
pub(crate) use wire_enum;
wire_enum!(AssetType);
pub(crate) fn known<T: Wire>(
field: &str,
value: Option<&T>,
) -> Result<(), crate::validation::ValidationError> {
match value {
Some(v) if v.is_unknown() => Err(crate::validation::ValidationError::new(
crate::validation::ValidationErrorKind::Unsupported,
Some(field),
format!("{field} is not a value Schwab accepts"),
)),
_ => Ok(()),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum AssetMainType {
Bond,
Equity,
Etf,
Extended,
Forex,
Future,
FutureOption,
Fundamental,
Index,
Indicator,
MutualFund,
Option,
#[serde(other)]
Unknown,
}
#[skip_serializing_none]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", default)]
#[non_exhaustive]
pub struct AccountsInstrument {
pub asset_type: AssetType,
pub symbol: String,
pub cusip: Option<String>,
pub description: Option<String>,
pub instrument_id: Option<i64>,
pub net_change: Option<f64>,
#[serde(rename = "type")]
pub type_: Option<String>,
#[serde(default, deserialize_with = "lenient")]
pub put_call: Option<PutCall>,
pub underlying_symbol: Option<String>,
pub option_multiplier: Option<i64>,
pub maturity_date: Option<String>,
pub factor: Option<f64>,
pub variable_rate: Option<f64>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[non_exhaustive]
pub enum AssetSubType {
Adr,
Cef,
Coe,
Etf,
Etn,
Gdr,
Mmf,
Oef,
Prf,
Rgt,
Uit,
War,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PutCall {
Put,
Call,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct PageInfo {
pub page_size: i32,
pub page_number: i32,
pub total_pages: i32,
pub total_items: i32,
}
pub fn normalize_integral_numbers(value: &mut serde_json::Value) {
use serde_json::Value;
const EXACT: f64 = 9_007_199_254_740_992.0;
match value {
Value::Number(number) if number.is_f64() => {
if let Some(float) = number.as_f64()
&& float.fract() == 0.0
&& float.abs() <= EXACT
{
*value = Value::from(float as i64);
}
}
Value::Array(items) => items.iter_mut().for_each(normalize_integral_numbers),
Value::Object(fields) => fields.values_mut().for_each(normalize_integral_numbers),
_ => {}
}
}
pub fn optional_id<'de, D: serde::Deserializer<'de>>(de: D) -> Result<Option<String>, D::Error> {
let value = Option::<serde_json::Value>::deserialize(de)?;
match value {
None => Ok(None),
Some(serde_json::Value::String(s)) => Ok(Some(s)),
Some(serde_json::Value::Number(n)) if n.is_u64() || n.is_i64() => Ok(Some(n.to_string())),
_ => Err(serde::de::Error::custom(
"Expected an integer or string identifier",
)),
}
}
pub fn lenient<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: DeserializeOwned,
{
let value = Option::<serde_json::Value>::deserialize(de)?;
Ok(value.and_then(|v| serde_json::from_value(v).ok()))
}
pub mod exact_decimal {
use rust_decimal::Decimal;
use serde::de::{self, Deserializer, Visitor};
use std::fmt;
pub fn parse(s: &str) -> Result<Decimal, String> {
let plain = !s.is_empty()
&& s.bytes()
.all(|b| b.is_ascii_digit() || b == b'.' || b == b'-' || b == b'+');
if !plain {
return Err(format!("{s:?} is not a plain decimal number"));
}
Decimal::from_str_exact(s).map_err(|e| format!("{s:?} is not an exact decimal: {e}"))
}
struct Exact;
impl Visitor<'_> for Exact {
type Value = Decimal;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a decimal number as a string, such as \"150.25\"")
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<Decimal, E> {
parse(s).map_err(E::custom)
}
fn visit_i64<E: de::Error>(self, n: i64) -> Result<Decimal, E> {
Ok(Decimal::from(n))
}
fn visit_u64<E: de::Error>(self, n: u64) -> Result<Decimal, E> {
Ok(Decimal::from(n))
}
fn visit_f64<E: de::Error>(self, n: f64) -> Result<Decimal, E> {
if !n.is_finite() {
return Err(E::custom("decimal must be finite"));
}
parse(&n.to_string()).map_err(E::custom)
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Decimal, D::Error> {
de.deserialize_any(Exact)
}
pub fn serialize_number<S: serde::Serializer>(
value: &Decimal,
s: S,
) -> Result<S::Ok, S::Error> {
use rust_decimal::prelude::ToPrimitive;
use serde::ser::Error;
if value.fract().is_zero()
&& let Some(n) = value.to_i64()
{
return s.serialize_i64(n);
}
match value.to_f64() {
Some(n) if parse(&n.to_string()).is_ok_and(|back| back == *value) => s.serialize_f64(n),
_ => Err(S::Error::custom(format!(
"{value} has more precision than a JSON number carries"
))),
}
}
pub fn serialize_string<S: serde::Serializer>(
value: &Decimal,
s: S,
) -> Result<S::Ok, S::Error> {
s.collect_str(value)
}
pub mod option {
use rust_decimal::Decimal;
use serde::{Deserialize, Deserializer, Serializer};
pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Option<Decimal>, D::Error> {
#[derive(Deserialize)]
struct Wrap(#[serde(deserialize_with = "super::deserialize")] Decimal);
Ok(Option::<Wrap>::deserialize(de)?.map(|Wrap(d)| d))
}
pub fn serialize_number<S: Serializer>(
value: &Option<Decimal>,
s: S,
) -> Result<S::Ok, S::Error> {
match value {
Some(value) => super::serialize_number(value, s),
None => s.serialize_none(),
}
}
pub fn serialize_string<S: Serializer>(
value: &Option<Decimal>,
s: S,
) -> Result<S::Ok, S::Error> {
match value {
Some(value) => super::serialize_string(value, s),
None => s.serialize_none(),
}
}
}
}
#[cfg(feature = "schemars")]
#[derive(Debug)]
pub struct DecimalString;
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for DecimalString {
fn schema_name() -> std::borrow::Cow<'static, str> {
"DecimalString".into()
}
fn inline_schema() -> bool {
true
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"pattern": "^[0-9]+(\\.[0-9]+)?$",
"description": "Exact decimal as a string, e.g. \"150.25\"; never a JSON number."
})
}
}
#[cfg(feature = "schemars")]
#[derive(Debug)]
pub struct QuantityNumber;
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for QuantityNumber {
fn schema_name() -> std::borrow::Cow<'static, str> {
"QuantityNumber".into()
}
fn inline_schema() -> bool {
true
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": ["number", "string"],
"pattern": "^[0-9]+(\\.[0-9]+)?$",
"description": "Shares, contracts or fund units: a JSON number such as 10, \
or a decimal string such as \"10\"."
})
}
}
pub fn schwab_timestamp(at: DateTime<Utc>) -> String {
at.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()
}
pub fn parse_schwab_time(text: &str) -> Result<DateTime<Utc>, chrono::ParseError> {
DateTime::parse_from_str(text, "%Y-%m-%dT%H:%M:%S%z")
.or_else(|_| DateTime::parse_from_str(text, "%Y-%m-%dT%H:%M:%S%.f%z"))
.or_else(|_| DateTime::parse_from_rfc3339(text))
.map(|at| at.with_timezone(&Utc))
}
pub(crate) fn serialize_timestamp<S: serde::Serializer>(
at: &DateTime<Utc>,
serializer: S,
) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&schwab_timestamp(*at))
}
pub(crate) fn serialize_optional_timestamp<S: serde::Serializer>(
at: &Option<DateTime<Utc>>,
serializer: S,
) -> Result<S::Ok, S::Error> {
match at {
Some(at) => serialize_timestamp(at, serializer),
None => serializer.serialize_none(),
}
}