Skip to main content

defuse_token_id/
lib.rs

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