wire/message_refs/
kind.rs1use objects::object::{
5 MarkerName, ReservedRefNameError, StateId, SyntheticFrontierName, SyntheticFrontierNameError,
6 ThreadName, is_reserved_heddle_namespace,
7};
8use serde::{Deserialize, Serialize};
9
10use super::RefEntry;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum RefKind {
19 Thread,
20 Marker,
21 SyntheticFrontierRoot,
22}
23
24impl RefKind {
25 pub fn from_advertised_name(name: &str, advertised_as_thread: bool) -> Self {
33 if SyntheticFrontierName::looks_like(name) || is_reserved_heddle_namespace(name) {
34 return RefKind::SyntheticFrontierRoot;
35 }
36 if advertised_as_thread {
37 RefKind::Thread
38 } else {
39 RefKind::Marker
40 }
41 }
42
43 pub fn is_user_thread(self) -> bool {
44 matches!(self, RefKind::Thread)
45 }
46
47 pub fn is_marker(self) -> bool {
48 matches!(self, RefKind::Marker)
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum AdvertisedRef {
56 Thread(ThreadName),
57 Marker(MarkerName),
58 SyntheticFrontier(SyntheticFrontierName),
59}
60
61#[derive(Debug, Clone, thiserror::Error)]
63pub enum AdvertisedRefError {
64 #[error("{0}")]
65 Reserved(#[from] ReservedRefNameError),
66 #[error("{0}")]
67 Synthetic(#[from] SyntheticFrontierNameError),
68 #[error("ref '{name}' is reserved and cannot be consumed as a {kind:?}")]
69 KindMismatch { name: String, kind: RefKind },
70}
71
72impl RefEntry {
73 pub fn thread(name: impl Into<String>, state_id: StateId) -> Self {
74 Self {
75 name: name.into(),
76 state_id,
77 kind: RefKind::Thread,
78 }
79 }
80
81 pub fn marker(name: impl Into<String>, state_id: StateId) -> Self {
82 Self {
83 name: name.into(),
84 state_id,
85 kind: RefKind::Marker,
86 }
87 }
88
89 pub fn synthetic_frontier(name: SyntheticFrontierName, state_id: StateId) -> Self {
90 Self {
91 name: name.as_name(),
92 state_id,
93 kind: RefKind::SyntheticFrontierRoot,
94 }
95 }
96
97 pub fn from_advertised(
98 name: impl Into<String>,
99 state_id: StateId,
100 advertised_as_thread: bool,
101 ) -> Self {
102 let name = name.into();
103 let kind = RefKind::from_advertised_name(&name, advertised_as_thread);
104 Self {
105 name,
106 state_id,
107 kind,
108 }
109 }
110
111 pub fn is_user_thread(&self) -> bool {
112 self.kind.is_user_thread()
113 }
114
115 pub fn is_marker(&self) -> bool {
116 self.kind.is_marker()
117 }
118
119 pub fn advertised(&self) -> Result<AdvertisedRef, AdvertisedRefError> {
125 match self.kind {
126 RefKind::Thread => {
127 if is_reserved_heddle_namespace(&self.name) {
128 return Err(AdvertisedRefError::KindMismatch {
129 name: self.name.clone(),
130 kind: self.kind,
131 });
132 }
133 Ok(AdvertisedRef::Thread(ThreadName::try_new(
134 self.name.clone(),
135 )?))
136 }
137 RefKind::Marker => {
138 if is_reserved_heddle_namespace(&self.name) {
139 return Err(AdvertisedRefError::KindMismatch {
140 name: self.name.clone(),
141 kind: self.kind,
142 });
143 }
144 Ok(AdvertisedRef::Marker(MarkerName::try_new(
145 self.name.clone(),
146 )?))
147 }
148 RefKind::SyntheticFrontierRoot => Ok(AdvertisedRef::SyntheticFrontier(
149 SyntheticFrontierName::parse(&self.name)?,
150 )),
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use objects::object::{ChangeId, StateId, SyntheticFrontierName};
158
159 use super::*;
160
161 fn cid(last: u8) -> ChangeId {
162 let mut bytes = [0u8; 16];
163 bytes[15] = last;
164 ChangeId::from_bytes(bytes)
165 }
166
167 #[test]
168 fn advertised_synthetic_root_never_becomes_thread_or_marker_name() {
169 let change = cid(11);
170 let name = SyntheticFrontierName::new("main", change).unwrap();
171 let entry = RefEntry::from_advertised(name.as_name(), StateId::from_bytes([1; 32]), true);
172 assert_eq!(entry.kind, RefKind::SyntheticFrontierRoot);
173 match entry.advertised().expect("synthetic consume") {
174 AdvertisedRef::SyntheticFrontier(parsed) => assert_eq!(parsed, name),
175 AdvertisedRef::Thread(_) | AdvertisedRef::Marker(_) => {
176 panic!("synthetic root must not construct ThreadName or MarkerName")
177 }
178 }
179 }
180
181 #[test]
182 fn advertised_as_thread_flag_cannot_reclassify_a_frontier_root() {
183 let change = cid(12);
184 let name = SyntheticFrontierName::new("main", change).unwrap();
185 assert_eq!(
186 RefKind::from_advertised_name(&name.as_name(), true),
187 RefKind::SyntheticFrontierRoot
188 );
189 assert_eq!(
190 RefKind::from_advertised_name(&name.as_name(), false),
191 RefKind::SyntheticFrontierRoot
192 );
193 assert_eq!(
194 RefKind::from_advertised_name("heddle/not-a-frontier", true),
195 RefKind::SyntheticFrontierRoot
196 );
197 }
198
199 #[test]
200 fn user_thread_at_hd_suffix_stays_a_thread() {
201 let entry = RefEntry::from_advertised("main@hd-abcdef", StateId::from_bytes([2; 32]), true);
202 assert_eq!(entry.kind, RefKind::Thread);
203 match entry.advertised().expect("user thread") {
204 AdvertisedRef::Thread(name) => assert_eq!(name.as_str(), "main@hd-abcdef"),
205 other => panic!("expected thread, got {other:?}"),
206 }
207 }
208}