1use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use std::{fmt, str::FromStr};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
8pub enum InvalidIdKind {
9 #[error("empty identifier")]
11 Empty,
12 #[error("identifier contains a control character")]
14 ControlCharacter,
15}
16
17#[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 #[must_use]
28 pub const fn kind(&self) -> InvalidIdKind {
29 self.kind
30 }
31 #[must_use]
33 pub fn value(&self) -> &str {
34 &self.value
35 }
36 #[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 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 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#[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}