strop_workspace/
container.rs1#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
10#[serde(try_from = "String", into = "String")]
11pub struct ContainerId(String);
12
13#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
15#[error("a container identity is the 64-hex inspect id, not a name or prefix")]
16pub struct ContainerIdError;
17
18impl ContainerId {
19 pub fn canonical(value: String) -> Result<Self, ContainerIdError> {
20 let valid = value.len() == 64
21 && value
22 .bytes()
23 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b));
24 if valid {
25 Ok(Self(value))
26 } else {
27 Err(ContainerIdError)
28 }
29 }
30
31 pub fn as_str(&self) -> &str {
32 &self.0
33 }
34}
35
36impl std::fmt::Display for ContainerId {
37 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 formatter.write_str(&self.0)
39 }
40}
41
42impl TryFrom<String> for ContainerId {
43 type Error = ContainerIdError;
44 fn try_from(value: String) -> Result<Self, Self::Error> {
45 Self::canonical(value)
46 }
47}
48impl From<ContainerId> for String {
49 fn from(value: ContainerId) -> Self {
50 value.0
51 }
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57
58 #[test]
59 fn canonical_ids_roundtrip_and_names_refuse() {
60 let id = ContainerId::canonical("a".repeat(64)).unwrap();
61 assert_eq!(id.as_str(), &"a".repeat(64));
62 assert!(ContainerId::canonical("name".into()).is_err());
63 assert!(
64 ContainerId::canonical("A".repeat(64)).is_err(),
65 "lowercase hex"
66 );
67 let json = serde_json::to_string(&id).unwrap();
68 assert_eq!(serde_json::from_str::<ContainerId>(&json).unwrap(), id);
69 assert!(serde_json::from_str::<ContainerId>("\"name\"").is_err());
70 }
71}