unc_account_id/
account_id.rs

1use std::{borrow::Cow, fmt, ops::Deref, str::FromStr};
2
3use crate::{AccountIdRef, ParseAccountError};
4
5/// Utility Account Identifier.
6///
7/// This is a unique, syntactically valid, human-readable account identifier on the Utility network.
8///
9/// [See the crate-level docs for information about validation.](index.html#account-id-rules)
10///
11/// Also see [Error kind precedence](AccountId#error-kind-precedence).
12///
13/// ## Examples
14///
15/// ```
16/// use unc_account_id::AccountId;
17///
18/// let alice: AccountId = "alice.unc".parse().unwrap();
19///
20/// assert!("ƒelicia.unc".parse::<AccountId>().is_err()); // (ƒ is not f)
21/// ```
22#[derive(Eq, Ord, Hash, Clone, Debug, PartialEq, PartialOrd)]
23#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
24#[cfg_attr(feature = "abi", derive(borsh::BorshSchema))]
25pub struct AccountId(pub(crate) Box<str>);
26
27impl AccountId {
28    /// Shortest valid length for a Utility Account ID.
29    pub const MIN_LEN: usize = crate::validation::MIN_LEN;
30    /// Longest valid length for a Utility Account ID.
31    pub const MAX_LEN: usize = crate::validation::MAX_LEN;
32
33    /// Creates an `AccountId` without any validation checks.
34    ///
35    /// Please note that this is restrictively for internal use only. Plus, being behind a feature flag,
36    /// this could be removed later in the future.
37    ///
38    /// ## Safety
39    ///
40    /// Since this skips validation and constructs an `AccountId` regardless,
41    /// the caller bears the responsibility of ensuring that the Account ID is valid.
42    /// You can use the [`AccountId::validate`] function sometime after its creation but before it's use.
43    ///
44    /// ## Examples
45    ///
46    /// ```
47    /// use unc_account_id::AccountId;
48    ///
49    /// let alice = AccountId::new_unvalidated("alice.unc".to_string());
50    /// assert!(AccountId::validate(alice.as_str()).is_ok());
51    /// ```
52    #[doc(hidden)]
53    #[cfg(feature = "internal_unstable")]
54    #[deprecated = "AccountId construction without validation is illegal"]
55    pub fn new_unvalidated(account_id: String) -> Self {
56        Self(account_id.into_boxed_str())
57    }
58
59    /// Validates a string as a well-structured Utility Account ID.
60    ///
61    /// Checks Account ID validity without constructing an `AccountId` instance.
62    ///
63    /// ## Examples
64    ///
65    /// ```
66    /// use unc_account_id::{AccountId, ParseErrorKind};
67    ///
68    /// assert!(AccountId::validate("alice.unc").is_ok());
69    ///
70    /// assert!(
71    ///   matches!(
72    ///     AccountId::validate("ƒelicia.unc"), // fancy ƒ!
73    ///     Err(err) if err.kind() == &ParseErrorKind::InvalidChar
74    ///   )
75    /// );
76    /// ```
77    ///
78    /// ## Error kind precedence
79    ///
80    /// If an Account ID has multiple format violations, the first one would be reported.
81    ///
82    /// ### Examples
83    ///
84    /// ```
85    /// use unc_account_id::{AccountId, ParseErrorKind};
86    ///
87    /// assert!(
88    ///   matches!(
89    ///     AccountId::validate("A__ƒƒluent."),
90    ///     Err(err) if err.kind() == &ParseErrorKind::InvalidChar
91    ///   )
92    /// );
93    ///
94    /// assert!(
95    ///   matches!(
96    ///     AccountId::validate("a__ƒƒluent."),
97    ///     Err(err) if err.kind() == &ParseErrorKind::RedundantSeparator
98    ///   )
99    /// );
100    ///
101    /// assert!(
102    ///   matches!(
103    ///     AccountId::validate("aƒƒluent."),
104    ///     Err(err) if err.kind() == &ParseErrorKind::InvalidChar
105    ///   )
106    /// );
107    ///
108    /// assert!(
109    ///   matches!(
110    ///     AccountId::validate("affluent."),
111    ///     Err(err) if err.kind() == &ParseErrorKind::RedundantSeparator
112    ///   )
113    /// );
114    /// ```
115    pub fn validate(account_id: &str) -> Result<(), ParseAccountError> {
116        crate::validation::validate(account_id)
117    }
118}
119
120impl AsRef<str> for AccountId {
121    fn as_ref(&self) -> &str {
122        &self.0
123    }
124}
125
126impl AsRef<AccountIdRef> for AccountId {
127    fn as_ref(&self) -> &AccountIdRef {
128        self
129    }
130}
131
132impl Deref for AccountId {
133    type Target = AccountIdRef;
134
135    fn deref(&self) -> &Self::Target {
136        AccountIdRef::new_unvalidated(&self.0)
137    }
138}
139
140impl std::borrow::Borrow<AccountIdRef> for AccountId {
141    fn borrow(&self) -> &AccountIdRef {
142        AccountIdRef::new_unvalidated(self)
143    }
144}
145
146impl FromStr for AccountId {
147    type Err = ParseAccountError;
148
149    fn from_str(account_id: &str) -> Result<Self, Self::Err> {
150        crate::validation::validate(account_id)?;
151        Ok(Self(account_id.into()))
152    }
153}
154
155impl TryFrom<Box<str>> for AccountId {
156    type Error = ParseAccountError;
157
158    fn try_from(account_id: Box<str>) -> Result<Self, Self::Error> {
159        crate::validation::validate(&account_id)?;
160        Ok(Self(account_id))
161    }
162}
163
164impl TryFrom<String> for AccountId {
165    type Error = ParseAccountError;
166
167    fn try_from(account_id: String) -> Result<Self, Self::Error> {
168        crate::validation::validate(&account_id)?;
169        Ok(Self(account_id.into_boxed_str()))
170    }
171}
172
173impl fmt::Display for AccountId {
174    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
175        fmt::Display::fmt(&self.0, f)
176    }
177}
178
179impl From<AccountId> for String {
180    fn from(account_id: AccountId) -> Self {
181        account_id.0.into_string()
182    }
183}
184
185impl From<AccountId> for Box<str> {
186    fn from(value: AccountId) -> Box<str> {
187        value.0
188    }
189}
190
191impl PartialEq<AccountId> for AccountIdRef {
192    fn eq(&self, other: &AccountId) -> bool {
193        &self.0 == other.as_str()
194    }
195}
196
197impl PartialEq<AccountIdRef> for AccountId {
198    fn eq(&self, other: &AccountIdRef) -> bool {
199        self.as_str() == &other.0
200    }
201}
202
203impl<'a> PartialEq<AccountId> for &'a AccountIdRef {
204    fn eq(&self, other: &AccountId) -> bool {
205        &self.0 == other.as_str()
206    }
207}
208
209impl<'a> PartialEq<&'a AccountIdRef> for AccountId {
210    fn eq(&self, other: &&'a AccountIdRef) -> bool {
211        self.as_str() == &other.0
212    }
213}
214
215impl PartialEq<AccountId> for String {
216    fn eq(&self, other: &AccountId) -> bool {
217        self == other.as_str()
218    }
219}
220
221impl PartialEq<String> for AccountId {
222    fn eq(&self, other: &String) -> bool {
223        self.as_str() == other
224    }
225}
226
227impl PartialEq<AccountId> for str {
228    fn eq(&self, other: &AccountId) -> bool {
229        self == other.as_str()
230    }
231}
232
233impl PartialEq<str> for AccountId {
234    fn eq(&self, other: &str) -> bool {
235        self.as_str() == other
236    }
237}
238
239impl<'a> PartialEq<AccountId> for &'a str {
240    fn eq(&self, other: &AccountId) -> bool {
241        *self == other.as_str()
242    }
243}
244
245impl<'a> PartialEq<&'a str> for AccountId {
246    fn eq(&self, other: &&'a str) -> bool {
247        self.as_str() == *other
248    }
249}
250
251impl PartialOrd<AccountId> for AccountIdRef {
252    fn partial_cmp(&self, other: &AccountId) -> Option<std::cmp::Ordering> {
253        self.0.partial_cmp(other.as_str())
254    }
255}
256
257impl PartialOrd<AccountIdRef> for AccountId {
258    fn partial_cmp(&self, other: &AccountIdRef) -> Option<std::cmp::Ordering> {
259        self.as_str().partial_cmp(&other.0)
260    }
261}
262
263impl<'a> PartialOrd<AccountId> for &'a AccountIdRef {
264    fn partial_cmp(&self, other: &AccountId) -> Option<std::cmp::Ordering> {
265        self.0.partial_cmp(other.as_str())
266    }
267}
268
269impl<'a> PartialOrd<&'a AccountIdRef> for AccountId {
270    fn partial_cmp(&self, other: &&'a AccountIdRef) -> Option<std::cmp::Ordering> {
271        self.as_str().partial_cmp(&other.0)
272    }
273}
274
275impl PartialOrd<AccountId> for String {
276    fn partial_cmp(&self, other: &AccountId) -> Option<std::cmp::Ordering> {
277        self.as_str().partial_cmp(other.as_str())
278    }
279}
280
281impl PartialOrd<String> for AccountId {
282    fn partial_cmp(&self, other: &String) -> Option<std::cmp::Ordering> {
283        self.as_str().partial_cmp(other.as_str())
284    }
285}
286
287impl PartialOrd<AccountId> for str {
288    fn partial_cmp(&self, other: &AccountId) -> Option<std::cmp::Ordering> {
289        self.partial_cmp(other.as_str())
290    }
291}
292
293impl PartialOrd<str> for AccountId {
294    fn partial_cmp(&self, other: &str) -> Option<std::cmp::Ordering> {
295        self.as_str().partial_cmp(other)
296    }
297}
298
299impl<'a> PartialOrd<AccountId> for &'a str {
300    fn partial_cmp(&self, other: &AccountId) -> Option<std::cmp::Ordering> {
301        self.partial_cmp(&other.as_str())
302    }
303}
304
305impl<'a> PartialOrd<&'a str> for AccountId {
306    fn partial_cmp(&self, other: &&'a str) -> Option<std::cmp::Ordering> {
307        self.as_str().partial_cmp(*other)
308    }
309}
310
311impl<'a> From<AccountId> for Cow<'a, AccountIdRef> {
312    fn from(value: AccountId) -> Self {
313        Cow::Owned(value)
314    }
315}
316
317impl<'a> From<&'a AccountId> for Cow<'a, AccountIdRef> {
318    fn from(value: &'a AccountId) -> Self {
319        Cow::Borrowed(value)
320    }
321}
322
323impl<'a> From<Cow<'a, AccountIdRef>> for AccountId {
324    fn from(value: Cow<'a, AccountIdRef>) -> Self {
325        value.into_owned()
326    }
327}
328
329#[cfg(feature = "arbitrary")]
330impl<'a> arbitrary::Arbitrary<'a> for AccountId {
331    fn size_hint(depth: usize) -> (usize, Option<usize>) {
332        <&AccountIdRef as arbitrary::Arbitrary>::size_hint(depth)
333    }
334
335    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
336        Ok(u.arbitrary::<&AccountIdRef>()?.into())
337    }
338
339    fn arbitrary_take_rest(u: arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
340        Ok(<&AccountIdRef as arbitrary::Arbitrary>::arbitrary_take_rest(u)?.into())
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    #[allow(unused_imports)]
347    use super::*;
348
349    #[test]
350    #[cfg(feature = "arbitrary")]
351    fn test_arbitrary() {
352        let corpus = [
353            ("a|bcd", None),
354            ("ab|cde", Some("ab")),
355            ("a_-b", None),
356            ("ab_-c", Some("ab")),
357            ("a", None),
358            ("miraclx.unc", Some("miraclx.unc")),
359            (
360                "01234567890123456789012345678901234567890123456789012345678901234",
361                None,
362            ),
363        ];
364
365        for (input, expected_output) in corpus {
366            assert!(input.len() <= u8::MAX as usize);
367            let data = [input.as_bytes(), &[input.len() as _]].concat();
368            let mut u = arbitrary::Unstructured::new(&data);
369
370            assert_eq!(
371                u.arbitrary::<AccountId>().map(Into::<String>::into).ok(),
372                expected_output.map(Into::<String>::into)
373            );
374        }
375    }
376    #[test]
377    #[cfg(feature = "schemars")]
378    fn test_schemars() {
379        let schema = schemars::schema_for!(AccountId);
380        let json_schema = serde_json::to_value(&schema).unwrap();
381        dbg!(&json_schema);
382        assert_eq!(
383            json_schema,
384            serde_json::json!({
385                    "$schema": "http://json-schema.org/draft-07/schema#",
386                    "description": "Utility Account Identifier.\n\nThis is a unique, syntactically valid, human-readable account identifier on the Utility network.\n\n[See the crate-level docs for information about validation.](index.html#account-id-rules)\n\nAlso see [Error kind precedence](AccountId#error-kind-precedence).\n\n## Examples\n\n``` use unc_account_id::AccountId;\n\nlet alice: AccountId = \"alice.unc\".parse().unwrap();\n\nassert!(\"ƒelicia.unc\".parse::<AccountId>().is_err()); // (ƒ is not f) ```",
387                    "title": "AccountId",
388                    "type": "string"
389                }
390            )
391        );
392    }
393}