1use serde::{Deserialize, Serialize};
15
16macro_rules! opaque_id {
17 ($name:ident, $doc:literal) => {
18 #[doc = $doc]
19 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
20 #[serde(transparent)]
21 pub struct $name(String);
22
23 impl $name {
24 pub fn as_str(&self) -> &str {
26 &self.0
27 }
28 }
29
30 impl std::fmt::Display for $name {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 f.write_str(&self.0)
33 }
34 }
35
36 impl From<String> for $name {
37 fn from(value: String) -> Self {
38 $name(value)
39 }
40 }
41
42 impl From<&str> for $name {
43 fn from(value: &str) -> Self {
44 $name(value.to_string())
45 }
46 }
47
48 impl AsRef<str> for $name {
49 fn as_ref(&self) -> &str {
50 &self.0
51 }
52 }
53 };
54}
55
56opaque_id!(
57 RepoId,
58 "Opaque `\"group/project\"` repo identity (protocol: the UI never\n\
59 parses it; each backend owns its grammar)."
60);
61
62opaque_id!(
63 Sha,
64 "Opaque content id: MUST change when content changes (the cache\n\
65 is content-keyed and immutable). A commit sha on backends that\n\
66 have them; whatever the adapter guarantees otherwise."
67);
68
69opaque_id!(
70 GitRef,
71 "A resolvable revision name — branch, tag, or commit sha — as the\n\
72 backend interprets it (`repo/tree`'s `ref` param). `None` at a\n\
73 call site means the default branch; this type never encodes it."
74);
75
76opaque_id!(
77 RepoPath,
78 "An opaque repository-relative path, not a host filesystem path."
79);
80opaque_id!(
81 OrgId,
82 "An opaque organization or group identity, independent of its displayed caption."
83);
84opaque_id!(
85 OwnerId,
86 "An opaque repository namespace; it does not imply a personal account or organization."
87);
88
89impl Sha {
90 pub fn short(&self) -> String {
91 self.as_str().chars().take(7).collect()
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98pub struct Generation<Domain> {
99 value: u64,
100 domain: std::marker::PhantomData<Domain>,
101}
102
103impl<Domain> Default for Generation<Domain> {
104 fn default() -> Self {
105 Self {
106 value: 0,
107 domain: std::marker::PhantomData,
108 }
109 }
110}
111
112impl<Domain: Copy + PartialEq> Generation<Domain> {
113 pub fn tick(&mut self) -> Self {
114 self.value = self
115 .value
116 .checked_add(1)
117 .expect("request generation exhausted");
118 *self
119 }
120
121 pub fn is_current(&self, other: Self) -> bool {
122 *self == other
123 }
124}
125
126impl<Domain> std::fmt::Display for Generation<Domain> {
127 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 self.value.fmt(formatter)
129 }
130}
131
132impl<Domain> serde::Serialize for Generation<Domain> {
135 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
136 serializer.serialize_u64(self.value)
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn generation_guards_drop_stale_captures() {
146 let mut clock = Generation::<()>::default();
147 let spawn = clock.tick();
148 assert!(clock.is_current(spawn));
149 let _newer = clock.tick();
150 assert!(!clock.is_current(spawn));
151 }
152}