defuse_token_id/
lib.rs

1mod error;
2
3#[cfg(feature = "imt")]
4pub mod imt;
5#[cfg(feature = "nep141")]
6pub mod nep141;
7#[cfg(feature = "nep171")]
8pub mod nep171;
9#[cfg(feature = "nep245")]
10pub mod nep245;
11
12#[cfg(not(any(
13    feature = "nep141",
14    feature = "nep171",
15    feature = "nep245",
16    feature = "imt"
17)))]
18compile_error!(
19    r#"At least one of these features should be enabled:
20- `nep141`
21- `nep171`
22- `nep245`
23- `imt`
24"#
25);
26
27use core::{
28    fmt::{self, Debug, Display},
29    str::FromStr,
30};
31use strum::{EnumDiscriminants, EnumIter, EnumString};
32
33pub use self::error::TokenIdError;
34
35// avoid dependency to nep_245::TokenId just to import String alias
36pub(crate) type Nep245TokenId = String;
37
38#[cfg_attr(any(feature = "arbitrary", test), derive(arbitrary::Arbitrary))]
39#[cfg_attr(
40    feature = "borsh",
41    derive(::borsh::BorshSerialize, ::borsh::BorshDeserialize),
42    cfg_attr(feature = "abi", derive(::borsh::BorshSchema)),
43    borsh(use_discriminant = true)
44)]
45#[cfg_attr(
46    feature = "serde",
47    derive(::serde_with::SerializeDisplay, ::serde_with::DeserializeFromStr)
48)]
49#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, EnumDiscriminants, derive_more::From)]
50#[strum_discriminants(
51    name(TokenIdType),
52    cfg_attr(
53        feature = "serde",
54        derive(::serde_with::SerializeDisplay, ::serde_with::DeserializeFromStr),
55        cfg_attr(
56            feature = "abi",
57            derive(::schemars::JsonSchema),
58            schemars(with = "String"),
59        )
60    ),
61    derive(strum::Display, EnumString, EnumIter),
62    strum(serialize_all = "snake_case"),
63    vis(pub)
64)]
65#[repr(u8)]
66// Private: Because we need construction to go through the TokenId struct to check for length
67pub enum TokenId {
68    #[cfg(feature = "nep141")]
69    Nep141(crate::nep141::Nep141TokenId) = 0,
70    #[cfg(feature = "nep171")]
71    Nep171(crate::nep171::Nep171TokenId) = 1,
72    #[cfg(feature = "nep245")]
73    Nep245(crate::nep245::Nep245TokenId) = 2,
74    #[cfg(feature = "imt")]
75    Imt(crate::imt::ImtTokenId) = 3,
76}
77
78impl Debug for TokenId {
79    #[inline]
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match self {
82            #[cfg(feature = "nep141")]
83            Self::Nep141(token_id) => {
84                write!(f, "{}:{}", TokenIdType::Nep141, token_id)
85            }
86            #[cfg(feature = "nep171")]
87            Self::Nep171(token_id) => {
88                write!(f, "{}:{}", TokenIdType::Nep171, token_id)
89            }
90            #[cfg(feature = "nep245")]
91            Self::Nep245(token_id) => {
92                write!(f, "{}:{}", TokenIdType::Nep245, token_id)
93            }
94            #[cfg(feature = "imt")]
95            Self::Imt(token_id) => {
96                write!(f, "{}:{}", TokenIdType::Imt, token_id)
97            }
98        }
99    }
100}
101
102impl Display for TokenId {
103    #[inline]
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        fmt::Debug::fmt(&self, f)
106    }
107}
108
109impl FromStr for TokenId {
110    type Err = TokenIdError;
111
112    #[inline]
113    fn from_str(s: &str) -> Result<Self, Self::Err> {
114        let (typ, data) = s
115            .split_once(':')
116            .ok_or(strum::ParseError::VariantNotFound)?;
117        match typ.parse()? {
118            #[cfg(feature = "nep141")]
119            TokenIdType::Nep141 => data.parse().map(Self::Nep141),
120            #[cfg(feature = "nep171")]
121            TokenIdType::Nep171 => data.parse().map(Self::Nep171),
122            #[cfg(feature = "nep245")]
123            TokenIdType::Nep245 => data.parse().map(Self::Nep245),
124            #[cfg(feature = "imt")]
125            TokenIdType::Imt => data.parse().map(Self::Imt),
126        }
127    }
128}
129
130#[cfg(feature = "abi")]
131const _: () = {
132    use schemars::{
133        JsonSchema,
134        r#gen::SchemaGenerator,
135        schema::{InstanceType, Schema, SchemaObject},
136    };
137
138    impl JsonSchema for TokenId {
139        fn schema_name() -> String {
140            stringify!(TokenId).to_string()
141        }
142
143        fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
144            use near_account_id::AccountId;
145
146            SchemaObject {
147                instance_type: Some(InstanceType::String.into()),
148                extensions: std::iter::once((
149                    "examples",
150                    [
151                        #[cfg(feature = "nep141")]
152                        Self::Nep141(crate::nep141::Nep141TokenId::new(
153                            "ft.near".parse::<AccountId>().unwrap(),
154                        )),
155                        #[cfg(feature = "nep171")]
156                        Self::Nep171(crate::nep171::Nep171TokenId::new(
157                            "nft.near".parse::<AccountId>().unwrap(),
158                            "token_id1",
159                        )),
160                        #[cfg(feature = "nep245")]
161                        Self::Nep245(crate::nep245::Nep245TokenId::new(
162                            "mt.near".parse::<AccountId>().unwrap(),
163                            "token_id1",
164                        )),
165                        #[cfg(feature = "imt")]
166                        Self::Imt(crate::imt::ImtTokenId::new(
167                            "imt.near".parse::<AccountId>().unwrap(),
168                            "token_id1",
169                        )),
170                    ]
171                    .map(|s| s.to_string())
172                    .to_vec()
173                    .into(),
174                ))
175                .map(|(k, v)| (k.to_string(), v))
176                .collect(),
177                ..Default::default()
178            }
179            .into()
180        }
181    }
182};
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use defuse_test_utils::random::make_arbitrary;
188    use rstest::rstest;
189
190    #[cfg(feature = "borsh")]
191    #[rstest]
192    #[trace]
193    #[cfg_attr(feature = "nep141", case("nep141:abc", "0003000000616263"))]
194    #[cfg_attr(
195        feature = "nep171",
196        case("nep171:abc:xyz", "01030000006162630300000078797a")
197    )]
198    #[cfg_attr(
199        feature = "nep245",
200        case("nep245:abc:xyz", "02030000006162630300000078797a")
201    )]
202    #[cfg_attr(feature = "imt", case("imt:abc:xyz", "03030000006162630300000078797a"))]
203    fn roundtrip_fixed(#[case] token_id_str: &str, #[case] borsh_expected_hex: &str) {
204        let token_id: TokenId = token_id_str.parse().unwrap();
205        let borsh_expected = hex::decode(borsh_expected_hex).unwrap();
206
207        let borsh_ser = borsh::to_vec(&token_id).unwrap();
208        assert_eq!(borsh_ser, borsh_expected);
209
210        let got: TokenId = borsh::from_slice(&borsh_ser).unwrap();
211        assert_eq!(got, token_id);
212        assert_eq!(got.to_string(), token_id_str);
213    }
214
215    #[cfg(feature = "borsh")]
216    #[rstest]
217    #[trace]
218    fn borsh_roundtrip(#[from(make_arbitrary)] token_id: TokenId) {
219        let ser = borsh::to_vec(&token_id).unwrap();
220        let got: TokenId = borsh::from_slice(&ser).unwrap();
221        assert_eq!(got, token_id);
222    }
223
224    #[rstest]
225    #[trace]
226    fn display_from_str_roundtrip(#[from(make_arbitrary)] token_id: TokenId) {
227        let s = token_id.to_string();
228        let got: TokenId = s.parse().unwrap();
229        assert_eq!(got, token_id);
230    }
231
232    #[cfg(feature = "serde")]
233    #[rstest]
234    #[trace]
235    fn serde_roundtrip(#[from(make_arbitrary)] token_id: TokenId) {
236        let ser = serde_json::to_vec(&token_id).unwrap();
237        let got: TokenId = serde_json::from_slice(&ser).unwrap();
238        assert_eq!(got, token_id);
239    }
240}