Skip to main content

dynomite/conf/
tokens.rs

1//! Token list parsing.
2//!
3//! `tokens:` and `dyn_seeds[*].tokens` are comma-separated big-int
4//! strings. The C reference's `derive_tokens` accepts an optional
5//! leading `-` per component and any number of decimal digits; the
6//! actual ring math is in `hashkit::token` (Stage 3). At configuration
7//! time we only need to validate the syntax and remember the original
8//! components so they can be re-emitted.
9
10use std::fmt;
11
12use serde::de::{self, Deserializer, Visitor};
13use serde::{Deserialize, Serialize};
14
15use super::error::ConfError;
16
17/// One element of a comma-separated token list.
18///
19/// # Examples
20///
21/// ```
22/// use dynomite::conf::TokenComponent;
23/// let pos = TokenComponent::parse("42").unwrap();
24/// assert_eq!(pos.signum, 1);
25/// assert_eq!(pos.digits, "42");
26/// let zero = TokenComponent::parse("0").unwrap();
27/// assert_eq!(zero.signum, 0);
28/// ```
29#[derive(Debug, Clone, Eq, PartialEq, Hash)]
30pub struct TokenComponent {
31    /// `-1` for negative, `0` for zero, `1` for positive.
32    pub signum: i8,
33    /// Decimal digits without the optional leading sign.
34    pub digits: String,
35}
36
37impl TokenComponent {
38    /// Parse a single component (no commas, no surrounding whitespace).
39    ///
40    /// # Examples
41    ///
42    /// ```
43    /// use dynomite::conf::TokenComponent;
44    /// let neg = TokenComponent::parse("-7").unwrap();
45    /// assert_eq!(neg.signum, -1);
46    /// assert_eq!(neg.digits, "7");
47    /// assert!(TokenComponent::parse("").is_err());
48    /// assert!(TokenComponent::parse("-").is_err());
49    /// assert!(TokenComponent::parse("12a").is_err());
50    /// ```
51    pub fn parse(raw: &str) -> Result<Self, ConfError> {
52        if raw.is_empty() {
53            return Err(ConfError::BadToken {
54                value: raw.to_string(),
55                reason: "empty token component".to_string(),
56            });
57        }
58        let (signum, digits): (i8, &str) = if let Some(rest) = raw.strip_prefix('-') {
59            if rest.is_empty() {
60                return Err(ConfError::BadToken {
61                    value: raw.to_string(),
62                    reason: "lone minus sign".to_string(),
63                });
64            }
65            (-1, rest)
66        } else if raw == "0" {
67            return Ok(Self {
68                signum: 0,
69                digits: "0".to_string(),
70            });
71        } else {
72            (1, raw)
73        };
74        if !digits.bytes().all(|b| b.is_ascii_digit()) {
75            return Err(ConfError::BadToken {
76                value: raw.to_string(),
77                reason: "non-digit character in token".to_string(),
78            });
79        }
80        Ok(Self {
81            signum,
82            digits: digits.to_string(),
83        })
84    }
85}
86
87impl fmt::Display for TokenComponent {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        if self.signum < 0 {
90            f.write_str("-")?;
91        }
92        f.write_str(&self.digits)
93    }
94}
95
96/// A list of [`TokenComponent`]s parsed from a comma-separated string.
97///
98/// # Examples
99///
100/// ```
101/// use dynomite::conf::TokenList;
102/// let t = TokenList::parse("0,1,2").unwrap();
103/// assert_eq!(t.len(), 3);
104/// assert_eq!(t.to_string(), "0,1,2");
105/// ```
106#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
107pub struct TokenList {
108    components: Vec<TokenComponent>,
109    raw: String,
110}
111
112impl TokenList {
113    /// Parse a comma-separated list. Leading or trailing whitespace
114    /// inside each component is rejected.
115    ///
116    /// # Examples
117    ///
118    /// ```
119    /// use dynomite::conf::TokenList;
120    /// assert_eq!(TokenList::parse("-7,0,9").unwrap().len(), 3);
121    /// assert!(TokenList::parse("").is_err());
122    /// assert!(TokenList::parse("1,,2").is_err());
123    /// ```
124    pub fn parse(raw: &str) -> Result<Self, ConfError> {
125        if raw.is_empty() {
126            return Err(ConfError::BadToken {
127                value: raw.to_string(),
128                reason: "empty token list".to_string(),
129            });
130        }
131        let mut components = Vec::new();
132        for piece in raw.split(',') {
133            components.push(TokenComponent::parse(piece)?);
134        }
135        Ok(Self {
136            components,
137            raw: raw.to_string(),
138        })
139    }
140
141    /// Borrow the parsed components.
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// use dynomite::conf::TokenList;
147    /// let t = TokenList::parse("1,2,3").unwrap();
148    /// assert_eq!(t.components().len(), 3);
149    /// ```
150    pub fn components(&self) -> &[TokenComponent] {
151        &self.components
152    }
153
154    /// Number of components in the list.
155    ///
156    /// # Examples
157    ///
158    /// ```
159    /// use dynomite::conf::TokenList;
160    /// assert_eq!(TokenList::parse("5").unwrap().len(), 1);
161    /// ```
162    pub fn len(&self) -> usize {
163        self.components.len()
164    }
165
166    /// Whether the list is empty (only constructible via the
167    /// `Default` impl).
168    ///
169    /// # Examples
170    ///
171    /// ```
172    /// use dynomite::conf::TokenList;
173    /// assert!(TokenList::default().is_empty());
174    /// assert!(!TokenList::parse("1").unwrap().is_empty());
175    /// ```
176    pub fn is_empty(&self) -> bool {
177        self.components.is_empty()
178    }
179
180    /// The original input string.
181    ///
182    /// # Examples
183    ///
184    /// ```
185    /// use dynomite::conf::TokenList;
186    /// assert_eq!(TokenList::parse("1,2").unwrap().raw(), "1,2");
187    /// ```
188    pub fn raw(&self) -> &str {
189        &self.raw
190    }
191}
192
193impl fmt::Display for TokenList {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        for (i, c) in self.components.iter().enumerate() {
196            if i > 0 {
197                f.write_str(",")?;
198            }
199            c.fmt(f)?;
200        }
201        Ok(())
202    }
203}
204
205impl Serialize for TokenList {
206    fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
207        ser.collect_str(self)
208    }
209}
210
211impl<'de> Deserialize<'de> for TokenList {
212    fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
213        struct V;
214        impl Visitor<'_> for V {
215            type Value = TokenList;
216            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217                f.write_str("a comma-separated big-integer token list")
218            }
219            fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
220                TokenList::parse(v).map_err(|e| E::custom(e.to_string()))
221            }
222            fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
223                self.visit_str(&v)
224            }
225            fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
226                self.visit_str(&v.to_string())
227            }
228            fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
229                self.visit_str(&v.to_string())
230            }
231        }
232        de.deserialize_any(V)
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn single_token() {
242        let t = TokenList::parse("101134286").unwrap();
243        assert_eq!(t.len(), 1);
244        assert_eq!(t.components()[0].signum, 1);
245        assert_eq!(t.components()[0].digits, "101134286");
246    }
247
248    #[test]
249    fn comma_separated() {
250        let t = TokenList::parse("0,1,2,4294967295").unwrap();
251        assert_eq!(t.len(), 4);
252        assert_eq!(t.to_string(), "0,1,2,4294967295");
253    }
254
255    #[test]
256    fn negative_token() {
257        let t = TokenList::parse("-7").unwrap();
258        assert_eq!(t.components()[0].signum, -1);
259        assert_eq!(t.components()[0].digits, "7");
260        assert_eq!(t.to_string(), "-7");
261    }
262
263    #[test]
264    fn zero_normalised() {
265        let t = TokenList::parse("0").unwrap();
266        assert_eq!(t.components()[0].signum, 0);
267    }
268
269    #[test]
270    fn empty_rejected() {
271        assert!(TokenList::parse("").is_err());
272    }
273
274    #[test]
275    fn non_digit_rejected() {
276        assert!(TokenList::parse("12a").is_err());
277    }
278
279    #[test]
280    fn lone_minus_rejected() {
281        assert!(TokenList::parse("-").is_err());
282    }
283
284    #[test]
285    fn empty_component_rejected() {
286        assert!(TokenList::parse("1,,2").is_err());
287    }
288}