Skip to main content

oanda_client/
ids.rs

1//! Validated OANDA provider identifiers.
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use std::{fmt, str::FromStr};
5
6/// Why an identifier was rejected at its public construction boundary.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
8pub enum InvalidIdKind {
9    /// An identity must contain at least one character.
10    #[error("empty identifier")]
11    Empty,
12    /// Provider identities cannot contain control characters.
13    #[error("identifier contains a control character")]
14    ControlCharacter,
15}
16
17/// A rejected provider identifier, retained for caller correction without cloning.
18#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
19#[error("invalid OANDA identifier: {kind}")]
20pub struct InvalidId {
21    value: String,
22    kind: InvalidIdKind,
23}
24
25impl InvalidId {
26    /// The reason construction failed.
27    #[must_use]
28    pub const fn kind(&self) -> InvalidIdKind {
29        self.kind
30    }
31    /// The rejected provider spelling.
32    #[must_use]
33    pub fn value(&self) -> &str {
34        &self.value
35    }
36    /// Recover ownership of the rejected value for correction.
37    #[must_use]
38    pub fn into_value(self) -> String {
39        self.value
40    }
41}
42
43macro_rules! id_type {
44    ($name:ident) => {
45        #[doc = concat!("Validated OANDA ", stringify!($name), ".")]
46        #[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
47        pub struct $name(String);
48
49        impl $name {
50            /// Validate and construct an identifier.
51            ///
52            /// # Errors
53            ///
54            /// Returns [`InvalidId`] for an empty identifier or one containing control characters.
55            pub fn new(value: impl Into<String>) -> Result<Self, InvalidId> {
56                let value = value.into();
57                if value.is_empty() {
58                    return Err(InvalidId {
59                        value,
60                        kind: InvalidIdKind::Empty,
61                    });
62                }
63                if value.chars().any(char::is_control) {
64                    return Err(InvalidId {
65                        value,
66                        kind: InvalidIdKind::ControlCharacter,
67                    });
68                }
69                Ok(Self(value))
70            }
71
72            /// Return the provider spelling.
73            pub fn as_str(&self) -> &str {
74                &self.0
75            }
76        }
77
78        impl fmt::Debug for $name {
79            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80                f.debug_tuple(stringify!($name)).field(&self.0).finish()
81            }
82        }
83        impl fmt::Display for $name {
84            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85                f.write_str(&self.0)
86            }
87        }
88        impl FromStr for $name {
89            type Err = InvalidId;
90            fn from_str(s: &str) -> Result<Self, Self::Err> {
91                Self::new(s)
92            }
93        }
94        impl Serialize for $name {
95            fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
96                serializer.serialize_str(&self.0)
97            }
98        }
99        impl<'de> Deserialize<'de> for $name {
100            fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
101                let value = String::deserialize(deserializer)?;
102                Self::new(value).map_err(serde::de::Error::custom)
103            }
104        }
105    };
106}
107
108id_type!(AccountID);
109id_type!(OrderID);
110id_type!(TradeID);
111id_type!(TransactionID);
112id_type!(InstrumentName);
113id_type!(OrderSpecifier);
114id_type!(TradeSpecifier);
115id_type!(ClientID);
116id_type!(ClientTag);
117id_type!(ClientComment);
118id_type!(RequestID);
119id_type!(ClientRequestID);
120id_type!(Currency);
121id_type!(CandleSpecification);
122id_type!(PricingComponent);
123
124#[cfg(test)]
125// Invalid-input fixtures fail immediately if construction unexpectedly succeeds.
126#[allow(clippy::expect_used)]
127mod tests {
128    use super::{AccountID, InvalidIdKind};
129
130    #[test]
131    fn rejected_identifiers_retain_the_consumed_spelling_and_reason() {
132        for (value, kind) in [
133            (String::new(), InvalidIdKind::Empty),
134            ("bad\nidentity".to_owned(), InvalidIdKind::ControlCharacter),
135        ] {
136            let error = AccountID::new(value.clone()).expect_err("invalid identifier");
137            assert_eq!(error.kind(), kind);
138            assert_eq!(error.value(), value);
139            assert_eq!(error.into_value(), value);
140        }
141    }
142}