radicle/identity/doc/
id.rs1use 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
10pub 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#[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 pub fn urn(&self) -> String {
53 format!("{RAD_PREFIX}{}", self.canonical())
54 }
55
56 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 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}