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}