Skip to main content

objects/object/
identifiers.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Newtype wrappers for string identifiers that were previously bare
3//! `String` / `&str`. The compiler enforces that a `ThreadName` cannot
4//! be passed where a `MarkerName` is expected, catching mix-ups at
5//! build time with zero runtime cost.
6//!
7//! Each type is `#[serde(transparent)]` so the on-disk / wire format
8//! is byte-identical to a bare `String`. Existing oplog entries,
9//! packed refs, and rmp-serde payloads decode unchanged.
10
11use std::{fmt, hash::Hash};
12
13use serde::{Deserialize, Serialize};
14
15macro_rules! string_newtype {
16    ($(#[$meta:meta])* $name:ident) => {
17        $(#[$meta])*
18        #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
19        #[serde(transparent)]
20        pub struct $name(pub String);
21
22        impl $name {
23            pub fn new(s: impl Into<String>) -> Self {
24                Self(s.into())
25            }
26
27            pub fn as_str(&self) -> &str {
28                &self.0
29            }
30
31            pub fn into_string(self) -> String {
32                self.0
33            }
34        }
35
36        impl fmt::Display for $name {
37            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38                f.write_str(&self.0)
39            }
40        }
41
42        impl AsRef<str> for $name {
43            fn as_ref(&self) -> &str {
44                &self.0
45            }
46        }
47
48        impl std::ops::Deref for $name {
49            type Target = str;
50            fn deref(&self) -> &str {
51                &self.0
52            }
53        }
54
55        impl From<String> for $name {
56            fn from(s: String) -> Self {
57                Self(s)
58            }
59        }
60
61        impl From<&str> for $name {
62            fn from(s: &str) -> Self {
63                Self(s.to_string())
64            }
65        }
66
67        impl From<$name> for String {
68            fn from(n: $name) -> String {
69                n.0
70            }
71        }
72
73        impl PartialEq<str> for $name {
74            fn eq(&self, other: &str) -> bool {
75                self.0 == other
76            }
77        }
78
79        impl PartialEq<&str> for $name {
80            fn eq(&self, other: &&str) -> bool {
81                self.0 == *other
82            }
83        }
84
85        impl PartialEq<String> for $name {
86            fn eq(&self, other: &String) -> bool {
87                self.0 == *other
88            }
89        }
90
91        impl std::borrow::Borrow<str> for $name {
92            fn borrow(&self) -> &str {
93                &self.0
94            }
95        }
96    };
97}
98
99string_newtype!(
100    /// Name of a heddle thread (branch-like construct).
101    ThreadName
102);
103
104string_newtype!(
105    /// Name of a heddle marker (tag-like construct).
106    MarkerName
107);
108
109string_newtype!(
110    /// Checkout/lane scope identifier for scoped operations.
111    Scope
112);
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn thread_name_display() {
120        let t = ThreadName::new("main");
121        assert_eq!(t.0, "main");
122        assert_eq!(t.0, "main");
123        assert_eq!(&*t, "main");
124    }
125
126    #[test]
127    fn serde_transparent_roundtrip() {
128        let t = ThreadName::new("feature/foo");
129        let json = serde_json::to_string(&t).unwrap();
130        assert_eq!(json, "\"feature/foo\"");
131        let back: ThreadName = serde_json::from_str(&json).unwrap();
132        assert_eq!(back, t);
133    }
134
135    #[test]
136    fn marker_name_distinct_from_thread_name() {
137        let _t: ThreadName = "main".into();
138        let _m: MarkerName = "v1.0".into();
139        // These are different types — the compiler prevents mixing them.
140    }
141
142    #[test]
143    #[allow(clippy::cmp_owned)] // exercising PartialEq<String> impl by design
144    fn comparison_with_str() {
145        let t = ThreadName::from("main");
146        assert!(t == "main");
147        assert!(t == *"main");
148        assert!(t == String::from("main"));
149    }
150
151    #[test]
152    fn borrow_for_hashmap_lookup() {
153        use std::collections::HashMap;
154        let mut map = HashMap::new();
155        map.insert(ThreadName::new("main"), 1);
156        assert_eq!(map.get("main"), Some(&1));
157    }
158}