1use std::fmt::{Debug, Display, Formatter};
2use std::str::{self, FromStr};
3
4use thiserror::Error;
5
6#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct GitOid {
15 bytes: [u8; 40],
16}
17
18impl GitOid {
19 pub fn as_str(&self) -> &str {
21 str::from_utf8(&self.bytes).unwrap()
22 }
23
24 pub fn as_short_str(&self) -> &str {
26 &self.as_str()[..16]
27 }
28
29 pub fn as_tiny_str(&self) -> &str {
31 &self.as_str()[..8]
32 }
33}
34
35impl Debug for GitOid {
36 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
37 write!(f, "GitOid(\"{}\")", self.as_str())
38 }
39}
40
41#[derive(Debug, Error, PartialEq)]
42pub enum OidParseError {
43 #[error("Object ID cannot be parsed from empty string")]
44 Empty,
45 #[error("Object ID must be exactly 40 hex characters")]
46 WrongLength,
47 #[error("Object ID must be valid hex characters")]
48 NotHex,
49}
50
51impl FromStr for GitOid {
52 type Err = OidParseError;
53
54 fn from_str(s: &str) -> Result<Self, Self::Err> {
55 if s.is_empty() {
56 return Err(OidParseError::Empty);
57 }
58
59 if s.len() != 40 {
60 return Err(OidParseError::WrongLength);
61 }
62
63 if !s.chars().all(|ch| ch.is_ascii_hexdigit()) {
64 return Err(OidParseError::NotHex);
65 }
66
67 let mut bytes = [0; 40];
68 bytes.copy_from_slice(s.as_bytes());
69 bytes.make_ascii_lowercase();
70 Ok(Self { bytes })
71 }
72}
73
74impl Display for GitOid {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 write!(f, "{}", self.as_str())
77 }
78}
79
80impl serde::Serialize for GitOid {
81 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
82 where
83 S: serde::Serializer,
84 {
85 self.as_str().serialize(serializer)
86 }
87}
88
89impl<'de> serde::Deserialize<'de> for GitOid {
90 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
91 where
92 D: serde::Deserializer<'de>,
93 {
94 struct Visitor;
95
96 impl serde::de::Visitor<'_> for Visitor {
97 type Value = GitOid;
98
99 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
100 f.write_str("a string")
101 }
102
103 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
104 GitOid::from_str(v).map_err(serde::de::Error::custom)
105 }
106 }
107
108 deserializer.deserialize_str(Visitor)
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use std::str::FromStr;
115
116 use super::{GitOid, OidParseError};
117
118 #[test]
119 fn git_oid() -> Result<(), OidParseError> {
120 assert_eq!(
121 GitOid::from_str("4a23745badf5bf5ef7928f1e346e9986bd696d82")?,
122 GitOid::from_str("4A23745BADF5BF5EF7928F1E346E9986BD696D82")?
123 );
124
125 assert_eq!(GitOid::from_str(""), Err(OidParseError::Empty));
126 assert_eq!(
127 GitOid::from_str(&str::repeat("a", 41)),
128 Err(OidParseError::WrongLength)
129 );
130 assert_eq!(
131 GitOid::from_str(&str::repeat("a", 39)),
132 Err(OidParseError::WrongLength)
133 );
134 assert_eq!(
135 GitOid::from_str(&str::repeat("x", 40)),
136 Err(OidParseError::NotHex)
137 );
138
139 Ok(())
140 }
141}