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