heddle_object_model/object/
identifiers.rs1use 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 ThreadName
102);
103
104string_newtype!(
105 MarkerName
107);
108
109pub const RESERVED_REF_SEGMENT: &str = "heddle";
114
115pub fn is_reserved_heddle_namespace(name: &str) -> bool {
120 let mut parts = name.split('/');
121 match (parts.next(), parts.next()) {
122 (Some(first), Some(_)) => first.eq_ignore_ascii_case(RESERVED_REF_SEGMENT),
123 _ => false,
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
129#[error(
130 "ref name '{name}' is reserved: the heddle/ namespace is internal and cannot be a user thread or marker"
131)]
132pub struct ReservedRefNameError {
133 pub name: String,
134}
135
136impl ThreadName {
137 pub fn try_new(s: impl Into<String>) -> Result<Self, ReservedRefNameError> {
141 let name = s.into();
142 if is_reserved_heddle_namespace(&name) {
143 return Err(ReservedRefNameError { name });
144 }
145 Ok(Self(name))
146 }
147}
148
149impl MarkerName {
150 pub fn try_new(s: impl Into<String>) -> Result<Self, ReservedRefNameError> {
154 let name = s.into();
155 if is_reserved_heddle_namespace(&name) {
156 return Err(ReservedRefNameError { name });
157 }
158 Ok(Self(name))
159 }
160}
161
162string_newtype!(
163 Scope
165);
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn thread_name_display() {
173 let t = ThreadName::new("main");
174 assert_eq!(t.0, "main");
175 assert_eq!(t.0, "main");
176 assert_eq!(&*t, "main");
177 }
178
179 #[test]
180 fn serde_transparent_roundtrip() {
181 let t = ThreadName::new("feature/foo");
182 let json = serde_json::to_string(&t).unwrap();
183 assert_eq!(json, "\"feature/foo\"");
184 let back: ThreadName = serde_json::from_str(&json).unwrap();
185 assert_eq!(back, t);
186 }
187
188 #[test]
189 fn marker_name_distinct_from_thread_name() {
190 let _t: ThreadName = "main".into();
191 let _m: MarkerName = "v1.0".into();
192 }
194
195 #[test]
196 #[allow(clippy::cmp_owned)] fn comparison_with_str() {
198 let t = ThreadName::from("main");
199 assert!(t == "main");
200 assert!(t == *"main");
201 assert!(t == String::from("main"));
202 }
203
204 #[test]
205 fn borrow_for_hashmap_lookup() {
206 use std::collections::HashMap;
207 let mut map = HashMap::new();
208 map.insert(ThreadName::new("main"), 1);
209 assert_eq!(map.get("main"), Some(&1));
210 }
211
212 #[test]
213 fn reserved_namespace_is_heddle_rooted_only() {
214 assert!(!is_reserved_heddle_namespace("heddle"));
215 assert!(!is_reserved_heddle_namespace("heddlefoo"));
216 assert!(!is_reserved_heddle_namespace("my/heddle"));
217 assert!(!is_reserved_heddle_namespace("main@review"));
218 assert!(is_reserved_heddle_namespace("heddle/frontier/main/hc-abc"));
219 assert!(is_reserved_heddle_namespace("Heddle/x"));
220 }
221
222 #[test]
223 fn try_new_rejects_reserved_thread_and_marker_names() {
224 assert!(ThreadName::try_new("heddle/frontier/main/hc-1").is_err());
225 assert!(MarkerName::try_new("heddle/notes").is_err());
226 assert_eq!(ThreadName::try_new("heddle").unwrap().as_str(), "heddle");
227 assert_eq!(
228 ThreadName::try_new("main@hd-abc").unwrap().as_str(),
229 "main@hd-abc"
230 );
231 }
232}