thegraph_core/
subgraph_id.rs

1use alloy::primitives::B256;
2
3/// Subgraph ID parsing error.
4#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
5pub enum ParseSubgraphIdError {
6    /// Invalid string length. The input string is longer than 44 characters.
7    #[error("invalid length {length}: {value} (length must be <=44)")]
8    InvalidLength { value: String, length: usize },
9
10    /// Invalid base-58 string. The input string contains invalid characters.
11    #[error("invalid character \"{value}\": {error}")]
12    InvalidCharacter { value: String, error: String },
13}
14
15/// A Subgraph ID is a 32-byte identifier for a subgraph.
16///
17/// ## Generating test data
18///
19/// The `SubgraphId` type implements the [`fake`] crate's [`fake::Dummy`] trait, allowing you to
20/// generate random `SubgraphId` values for testing.
21///
22/// Note that the `fake` feature must be enabled to use this functionality.
23///
24/// See the [`Dummy`] trait impl for usage examples.
25///
26/// [`Dummy`]: #impl-Dummy<Faker>-for-SubgraphId
27#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28#[cfg_attr(
29    feature = "serde",
30    derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr)
31)]
32#[repr(transparent)]
33pub struct SubgraphId(B256);
34
35impl SubgraphId {
36    /// The "zero" [`SubgraphId`].
37    ///
38    /// This is a constant value that represents the zero ID. It is equivalent to parsing a zeroed
39    /// 32-byte array.
40    pub const ZERO: Self = Self(B256::ZERO);
41
42    /// Create a new [`SubgraphId`].
43    pub const fn new(value: B256) -> Self {
44        Self(value)
45    }
46
47    /// Get the bytes of the [`SubgraphId`] as a slice.
48    pub fn as_bytes(&self) -> &[u8; 32] {
49        self.0.as_ref()
50    }
51}
52
53impl AsRef<B256> for SubgraphId {
54    fn as_ref(&self) -> &B256 {
55        &self.0
56    }
57}
58
59impl AsRef<[u8]> for SubgraphId {
60    fn as_ref(&self) -> &[u8] {
61        self.0.as_ref()
62    }
63}
64
65impl AsRef<[u8; 32]> for SubgraphId {
66    fn as_ref(&self) -> &[u8; 32] {
67        self.0.as_ref()
68    }
69}
70
71impl std::borrow::Borrow<[u8]> for SubgraphId {
72    fn borrow(&self) -> &[u8] {
73        self.0.borrow()
74    }
75}
76
77impl std::borrow::Borrow<[u8; 32]> for SubgraphId {
78    fn borrow(&self) -> &[u8; 32] {
79        self.0.borrow()
80    }
81}
82
83impl std::ops::Deref for SubgraphId {
84    type Target = B256;
85
86    fn deref(&self) -> &Self::Target {
87        &self.0
88    }
89}
90
91impl From<B256> for SubgraphId {
92    fn from(bytes: B256) -> Self {
93        Self(bytes)
94    }
95}
96
97impl From<[u8; 32]> for SubgraphId {
98    fn from(value: [u8; 32]) -> Self {
99        Self(value.into())
100    }
101}
102
103impl<'a> From<&'a [u8; 32]> for SubgraphId {
104    fn from(value: &'a [u8; 32]) -> Self {
105        Self(value.into())
106    }
107}
108
109impl<'a> TryFrom<&'a [u8]> for SubgraphId {
110    type Error = <B256 as TryFrom<&'a [u8]>>::Error;
111
112    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
113        value.try_into().map(Self)
114    }
115}
116
117impl From<SubgraphId> for B256 {
118    fn from(id: SubgraphId) -> Self {
119        id.0
120    }
121}
122
123impl From<&SubgraphId> for B256 {
124    fn from(id: &SubgraphId) -> Self {
125        id.0
126    }
127}
128
129impl std::str::FromStr for SubgraphId {
130    type Err = ParseSubgraphIdError;
131
132    /// Parse a `SubgraphID` from a base-58 encoded string.
133    fn from_str(value: &str) -> Result<Self, Self::Err> {
134        let mut buffer = [0_u8; 32];
135
136        // Decode the base58 string into a byte array, and get the number of bytes written
137        let len = bs58::decode(value)
138            .onto(&mut buffer)
139            .map_err(|err| match err {
140                bs58::decode::Error::BufferTooSmall => ParseSubgraphIdError::InvalidLength {
141                    value: value.to_string(),
142                    length: value.len(),
143                },
144                bs58::decode::Error::InvalidCharacter { .. } => {
145                    ParseSubgraphIdError::InvalidCharacter {
146                        value: value.to_string(),
147                        error: err.to_string(),
148                    }
149                }
150                bs58::decode::Error::NonAsciiCharacter { .. } => {
151                    ParseSubgraphIdError::InvalidCharacter {
152                        value: value.to_string(),
153                        error: err.to_string(),
154                    }
155                }
156                _ => unreachable!(),
157            })?;
158
159        // If the decoded hash is not 32 bytes long, rotate it to the right so the zero bytes
160        // are at the beginning of the array.
161        if len < 32 {
162            buffer.rotate_right(32 - len);
163        }
164
165        Ok(Self::from(buffer))
166    }
167}
168
169impl std::fmt::Display for SubgraphId {
170    /// Format the `SubgraphId` as a base58-encoded string.
171    ///
172    /// ```rust
173    /// # use thegraph_core::{subgraph_id, SubgraphId};
174    /// const ID: SubgraphId = subgraph_id!("DZz4kDTdmzWLWsV373w2bSmoar3umKKH9y82SUKr5qmp");
175    ///
176    /// assert_eq!(format!("{}", ID), "DZz4kDTdmzWLWsV373w2bSmoar3umKKH9y82SUKr5qmp");
177    /// ```
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        let leading_zeroes = self.0.iter().take_while(|b| **b == 0).count();
180        f.write_str(&bs58::encode(&self.0[leading_zeroes..]).into_string())
181    }
182}
183
184impl std::fmt::Debug for SubgraphId {
185    /// Format the `SubgraphId` as a debug string.
186    ///
187    /// ```rust
188    /// # use thegraph_core::{subgraph_id, SubgraphId};
189    /// const ID: SubgraphId = subgraph_id!("DZz4kDTdmzWLWsV373w2bSmoar3umKKH9y82SUKr5qmp");
190    ///
191    /// assert_eq!(format!("{:?}", ID), "SubgraphId(DZz4kDTdmzWLWsV373w2bSmoar3umKKH9y82SUKr5qmp)");
192    /// ```
193    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194        write!(f, "SubgraphId({self})")
195    }
196}
197
198#[cfg(feature = "fake")]
199/// To use the [`fake`] crate to generate random [`SubgraphId`] values, **the `fake` feature must
200/// be enabled.**
201///
202/// ```rust
203/// # use thegraph_core::SubgraphId;
204/// # use fake::Fake;
205/// let subgraph_id = fake::Faker.fake::<SubgraphId>();
206///
207/// println!("SubgraphId: {}", subgraph_id);
208/// ```
209impl fake::Dummy<fake::Faker> for SubgraphId {
210    fn dummy_with_rng<R: fake::Rng + ?Sized>(config: &fake::Faker, rng: &mut R) -> Self {
211        <[u8; 32]>::dummy_with_rng(config, rng).into()
212    }
213}
214
215/// Converts a sequence of string literals containing 32-bytes Base58-encoded data into a new
216/// [`SubgraphId`] at compile time.
217///
218/// To create an `SubgraphId` from a string literal (Base58) at compile time:
219///
220/// ```rust
221/// # use thegraph_core::{subgraph_id, SubgraphId};
222/// const SUBGRAPH_ID: SubgraphId = subgraph_id!("DZz4kDTdmzWLWsV373w2bSmoar3umKKH9y82SUKr5qmp");
223/// ```
224///
225/// If no argument is provided, the macro will create an `SubgraphId` with the zero ID:
226///
227/// ```rust
228/// # use thegraph_core::{subgraph_id, SubgraphId};
229/// const SUBGRAPH_ID: SubgraphId = subgraph_id!();
230///
231/// assert_eq!(SUBGRAPH_ID, SubgraphId::ZERO);
232/// ```
233#[macro_export]
234#[doc(hidden)]
235macro_rules! __subgraph_id {
236    () => {
237        $crate::SubgraphId::ZERO
238    };
239    ($id:tt) => {
240        $crate::SubgraphId::new($crate::__parse_subgraph_id_const($id))
241    };
242}
243
244/// Parse a base58-encoded string into a 32-bytes array at compile time.
245#[doc(hidden)]
246pub const fn __parse_subgraph_id_const(value: &str) -> B256 {
247    let data = value.as_bytes();
248    let bytes = bs58::decode(data).into_array_const_unwrap::<32>();
249    B256::new(bytes)
250}
251
252#[cfg(test)]
253mod tests {
254    use alloy::primitives::{B256, b256};
255
256    use super::{ParseSubgraphIdError, SubgraphId};
257    use crate::subgraph_id;
258
259    const VALID_SUBGRAPH_ID: &str = "7xB3yxxD8okmq4dZPky3eP1nYRgLfZrwMyUQBGo32t4U";
260
261    const EXPECTED_ID_BYTES: B256 =
262        b256!("67486e65165b1474898247760a4b852d70d95782c6325960e5b6b4fd82fed1bd");
263
264    const EXPECTED_ID: SubgraphId = subgraph_id!(VALID_SUBGRAPH_ID);
265
266    #[test]
267    fn parse_valid_string() {
268        //* Given
269        let valid_id = VALID_SUBGRAPH_ID;
270
271        //* When
272        let result = valid_id.parse::<SubgraphId>();
273
274        //* Then
275        let id = result.expect("invalid subgraph ID");
276        assert_eq!(id, EXPECTED_ID);
277        assert_eq!(id.0, EXPECTED_ID_BYTES);
278    }
279
280    #[test]
281    fn parse_failure_on_invalid_string() {
282        //* Given
283        // The following string is not a valid base58 string as it contains the `l` character
284        let invalid_id = "invalid";
285
286        //* When
287        let result = invalid_id.parse::<SubgraphId>();
288
289        //* Then
290        let err = result.expect_err("expected an error");
291        assert_eq!(
292            err,
293            ParseSubgraphIdError::InvalidCharacter {
294                value: invalid_id.to_string(),
295                error: "provided string contained invalid character 'l' at byte 4".to_string(),
296            }
297        );
298    }
299
300    #[test]
301    fn format_subgraph_id_display() {
302        //* Given
303        let valid_id = EXPECTED_ID;
304
305        //* When
306        let result = format!("{}", valid_id);
307
308        //* Then
309        assert_eq!(result, VALID_SUBGRAPH_ID);
310    }
311
312    #[test]
313    fn format_subgraph_id_debug() {
314        //* Given
315        let expected_debug_repr = format!("SubgraphId({})", VALID_SUBGRAPH_ID);
316
317        let valid_id = EXPECTED_ID;
318
319        //* When
320        let result = format!("{:?}", valid_id);
321
322        //* Then
323        assert_eq!(result, expected_debug_repr);
324    }
325
326    #[test]
327    fn serialize_leading_zeroes() {
328        let input = "4JruhWH1ZdwvUuMg2xCmtnZQYYHvmEq6cmTcZkpM6pW";
329        let output: SubgraphId = input.parse().unwrap();
330        assert_eq!(output.to_string(), input.to_string());
331    }
332}