Skip to main content

rustyschwab_types/
validation.rs

1//! Typed errors for request validation (`OrderRequest::validate` and the
2//! query-parameter validators).
3//!
4//! The message is written for a person; [`ValidationError::kind`] and
5//! [`ValidationError::field`] are for code that branches on the failure.
6
7use std::fmt;
8
9/// What a [`ValidationError`] reports.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11#[non_exhaustive]
12pub enum ValidationErrorKind {
13    /// A required field or part is missing: a symbol, a leg, the order
14    /// type a leg-bearing order needs.
15    Missing,
16    /// A field this request does not take: a price on a market order, a
17    /// mutual-fund field on an equity leg.
18    NotAllowed,
19    /// A value outside Schwab's bounds or tables: not positive, a reversed
20    /// window, a period its period type does not list.
21    OutOfRange,
22    /// A value that can appear in Schwab's responses but cannot be sent: an
23    /// `Unknown` enum value, or an order type, strategy or asset type that
24    /// cannot be submitted.
25    Unsupported,
26    /// A malformed symbol, including an OSI option symbol.
27    Symbol,
28    /// A fractional quantity where only whole shares or contracts trade.
29    NotWhole,
30    /// The order's shape: strategy nesting, OCO and TRIGGER children, how
31    /// legs and quantities relate, which sessions and durations fit.
32    Structure,
33    /// A price that is not a multiple of the supplied tick size.
34    Tick,
35}
36
37/// A request refused before anything is sent, with the field at fault in
38/// Schwab's wire spelling when there is one (`price`,
39/// `orderLegCollection[1].quantity`, `childOrderStrategies[0].stopPrice`).
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct ValidationError {
42    kind: ValidationErrorKind,
43    field: Option<String>,
44    message: String,
45}
46
47impl ValidationError {
48    pub(crate) fn new(
49        kind: ValidationErrorKind,
50        field: Option<&str>,
51        message: impl Into<String>,
52    ) -> Self {
53        Self {
54            kind,
55            field: field.map(str::to_owned),
56            message: message.into(),
57        }
58    }
59
60    /// What kind of problem this is.
61    pub fn kind(&self) -> ValidationErrorKind {
62        self.kind
63    }
64
65    /// The field at fault, when one is, in Schwab's wire spelling.
66    pub fn field(&self) -> Option<&str> {
67        self.field.as_deref()
68    }
69
70    /// The explanation, as [`Display`](fmt::Display) prints it.
71    pub fn message(&self) -> &str {
72        &self.message
73    }
74
75    /// Place a nested request's field under its parent: `prefix` is the
76    /// parent's field and index, such as `orderLegCollection[1]`.
77    pub(crate) fn within(mut self, prefix: &str) -> Self {
78        self.field = Some(match self.field {
79            Some(field) => format!("{prefix}.{field}"),
80            None => prefix.to_owned(),
81        });
82        self
83    }
84}
85
86impl fmt::Display for ValidationError {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        f.write_str(&self.message)
89    }
90}
91
92impl std::error::Error for ValidationError {}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn nested_fields_are_placed_under_their_parents() {
100        let error = ValidationError::new(ValidationErrorKind::NotWhole, Some("quantity"), "whole")
101            .within("orderLegCollection[1]")
102            .within("childOrderStrategies[0]");
103        assert_eq!(
104            error.field(),
105            Some("childOrderStrategies[0].orderLegCollection[1].quantity")
106        );
107        assert_eq!(error.to_string(), "whole");
108        assert_eq!(error.kind(), ValidationErrorKind::NotWhole);
109    }
110}