celestia_tendermint/chain/
id.rs

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