radicle/identity/doc/
id.rs

1use std::ops::Deref;
2use std::{ffi::OsString, fmt, str::FromStr};
3
4use git_ext::ref_format::{Component, RefString};
5use thiserror::Error;
6
7use crate::git;
8use crate::serde_ext;
9
10/// Radicle identifier prefix.
11pub const RAD_PREFIX: &str = "rad:";
12
13#[derive(Error, Debug)]
14pub enum IdError {
15    #[error("invalid git object id: {0}")]
16    InvalidOid(#[from] git2::Error),
17    #[error(transparent)]
18    Multibase(#[from] multibase::Error),
19}
20
21/// A repository identifier.
22#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
23#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
24pub struct RepoId(
25    #[cfg_attr(feature = "schemars", schemars(
26        with = "String",
27        description = "A repository identifier. Starts with \"rad:\", followed by a multibase Base58 encoded Git object identifier.",
28        regex(pattern = r"rad:z[1-9a-km-zA-HJ-NP-Z]+"),
29        length(min = 5),
30        example = &"rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
31    ))]
32    git::Oid,
33);
34
35impl fmt::Display for RepoId {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        f.write_str(self.urn().as_str())
38    }
39}
40
41impl fmt::Debug for RepoId {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        write!(f, "RepoId({self})")
44    }
45}
46
47impl RepoId {
48    /// Format the identifier as a human-readable URN.
49    ///
50    /// Eg. `rad:z3XncAdkZjeK9mQS5Sdc4qhw98BUX`.
51    ///
52    pub fn urn(&self) -> String {
53        format!("{RAD_PREFIX}{}", self.canonical())
54    }
55
56    /// Parse an identifier from the human-readable URN format.
57    /// Accepts strings without the radicle prefix as well,
58    /// for convenience.
59    pub fn from_urn(s: &str) -> Result<Self, IdError> {
60        let s = s.strip_prefix(RAD_PREFIX).unwrap_or(s);
61        let id = Self::from_canonical(s)?;
62
63        Ok(id)
64    }
65
66    /// Format the identifier as a multibase string.
67    ///
68    /// Eg. `z3XncAdkZjeK9mQS5Sdc4qhw98BUX`.
69    ///
70    pub fn canonical(&self) -> String {
71        multibase::encode(multibase::Base::Base58Btc, self.0.as_bytes())
72    }
73
74    pub fn from_canonical(input: &str) -> Result<Self, IdError> {
75        let (_, bytes) = multibase::decode(input)?;
76        let array: git::Oid = bytes.as_slice().try_into()?;
77
78        Ok(Self(array))
79    }
80}
81
82impl FromStr for RepoId {
83    type Err = IdError;
84
85    fn from_str(s: &str) -> Result<Self, Self::Err> {
86        Self::from_urn(s)
87    }
88}
89
90impl TryFrom<OsString> for RepoId {
91    type Error = IdError;
92
93    fn try_from(value: OsString) -> Result<Self, Self::Error> {
94        let string = value.to_string_lossy();
95        Self::from_canonical(&string)
96    }
97}
98
99impl From<git::Oid> for RepoId {
100    fn from(oid: git::Oid) -> Self {
101        Self(oid)
102    }
103}
104
105impl From<git2::Oid> for RepoId {
106    fn from(oid: git2::Oid) -> Self {
107        Self(oid.into())
108    }
109}
110
111impl Deref for RepoId {
112    type Target = git::Oid;
113
114    fn deref(&self) -> &Self::Target {
115        &self.0
116    }
117}
118
119impl serde::Serialize for RepoId {
120    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
121    where
122        S: serde::Serializer,
123    {
124        serde_ext::string::serialize(&self.urn(), serializer)
125    }
126}
127
128impl<'de> serde::Deserialize<'de> for RepoId {
129    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
130    where
131        D: serde::Deserializer<'de>,
132    {
133        serde_ext::string::deserialize(deserializer)
134    }
135}
136
137impl From<&RepoId> for Component<'_> {
138    fn from(id: &RepoId) -> Self {
139        let refstr =
140            RefString::try_from(id.0.to_string()).expect("repository id's are valid ref strings");
141        Component::from_refstr(refstr).expect("repository id's are valid refname components")
142    }
143}
144
145#[cfg(test)]
146#[allow(clippy::unwrap_used)]
147mod test {
148    use super::*;
149    use qcheck_macros::quickcheck;
150
151    #[quickcheck]
152    fn prop_from_str(input: RepoId) {
153        let encoded = input.to_string();
154        let decoded = RepoId::from_str(&encoded).unwrap();
155
156        assert_eq!(input, decoded);
157    }
158}