near_account_id/
account_id.rs

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