tendermint/chain/
id.rs

1//! Tendermint blockchain identifiers
2
3use core::{
4    cmp::Ordering,
5    fmt::{self, Debug, Display},
6    hash::{Hash, Hasher},
7    str::{self, FromStr},
8};
9
10use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
11use tendermint_proto::Protobuf;
12
13use crate::{error::Error, prelude::*};
14
15/// Maximum length of a `chain::Id` name. Matches `MaxChainIDLen` from:
16/// <https://github.com/tendermint/tendermint/blob/develop/types/genesis.go>
17// TODO: update this when `chain::Id` is derived from a digest output
18pub const MAX_LENGTH: usize = 50;
19
20/// Chain identifier (e.g. 'gaia-9000')
21#[derive(Clone)]
22pub struct Id(String);
23
24impl Protobuf<String> for Id {}
25
26impl TryFrom<String> for Id {
27    type Error = Error;
28
29    fn try_from(value: String) -> Result<Self, Self::Error> {
30        if value.is_empty() || value.len() > MAX_LENGTH {
31            return Err(Error::length());
32        }
33
34        for byte in value.as_bytes() {
35            match byte {
36                b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'.' => (),
37                _ => return Err(Error::parse("chain id charset".to_string())),
38            }
39        }
40
41        Ok(Id(value))
42    }
43}
44
45impl From<Id> for String {
46    fn from(value: Id) -> Self {
47        value.0
48    }
49}
50
51impl Id {
52    /// Get the chain ID as a `str`
53    pub fn as_str(&self) -> &str {
54        self.0.as_str()
55    }
56
57    /// Get the chain ID as a raw bytes.
58    pub fn as_bytes(&self) -> &[u8] {
59        self.0.as_bytes()
60    }
61}
62
63impl AsRef<str> for Id {
64    fn as_ref(&self) -> &str {
65        self.0.as_str()
66    }
67}
68
69impl Debug for Id {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(f, "chain::Id({})", self.0.as_str())
72    }
73}
74
75impl Display for Id {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        write!(f, "{}", self.0)
78    }
79}
80
81impl<'a> TryFrom<&'a str> for Id {
82    type Error = Error;
83
84    fn try_from(s: &str) -> Result<Self, Self::Error> {
85        Self::try_from(s.to_string())
86    }
87}
88
89impl FromStr for Id {
90    type Err = Error;
91    /// Parses string to create a new chain ID
92    fn from_str(name: &str) -> Result<Self, Error> {
93        Self::try_from(name.to_string())
94    }
95}
96
97impl Hash for Id {
98    fn hash<H: Hasher>(&self, state: &mut H) {
99        self.0.as_str().hash(state)
100    }
101}
102
103impl PartialOrd for Id {
104    fn partial_cmp(&self, other: &Id) -> Option<Ordering> {
105        Some(self.cmp(other))
106    }
107}
108
109impl Ord for Id {
110    fn cmp(&self, other: &Id) -> Ordering {
111        self.0.as_str().cmp(other.as_str())
112    }
113}
114
115impl PartialEq for Id {
116    fn eq(&self, other: &Id) -> bool {
117        self.0.as_str() == other.as_str()
118    }
119}
120
121impl Eq for Id {}
122
123impl Serialize for Id {
124    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
125        self.to_string().serialize(serializer)
126    }
127}
128
129impl<'de> Deserialize<'de> for Id {
130    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
131        Self::from_str(&String::deserialize(deserializer)?)
132            .map_err(|e| D::Error::custom(format!("{e}")))
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::error::ErrorDetail;
140
141    const EXAMPLE_CHAIN_ID: &str = "gaia-9000";
142
143    #[test]
144    fn parses_valid_chain_ids() {
145        assert_eq!(
146            EXAMPLE_CHAIN_ID.parse::<Id>().unwrap().as_str(),
147            EXAMPLE_CHAIN_ID
148        );
149
150        let long_id = String::from_utf8(vec![b'x'; MAX_LENGTH]).unwrap();
151        assert_eq!(&long_id.parse::<Id>().unwrap().as_str(), &long_id);
152    }
153
154    #[test]
155    fn rejects_empty_chain_ids() {
156        match "".parse::<Id>().unwrap_err().detail() {
157            ErrorDetail::Length(_) => {},
158            _ => panic!("expected length error"),
159        }
160    }
161
162    #[test]
163    fn rejects_overlength_chain_ids() {
164        let overlong_id = String::from_utf8(vec![b'x'; MAX_LENGTH + 1]).unwrap();
165        match overlong_id.parse::<Id>().unwrap_err().detail() {
166            ErrorDetail::Length(_) => {},
167            _ => panic!("expected length error"),
168        }
169    }
170}