blockworx_store/
record.rs1use blockworx_doc::id::BlockId;
14use serde::{Deserialize, Serialize};
15
16const GENESIS: &str = "blockworx.manifest.v1";
21
22const UNATTRIBUTED: &str = "unattributed author";
26
27#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)]
33#[serde(transparent)]
34pub struct WallTime(u64);
35
36impl WallTime {
37 pub const EPOCH: Self = Self(0);
38
39 pub const fn from_unix_millis(millis: u64) -> Self {
40 Self(millis)
41 }
42
43 pub const fn unix_millis(self) -> u64 {
44 self.0
45 }
46}
47
48#[derive(Clone, Copy, PartialEq, Eq)]
52pub struct Digest([u8; 32]);
53
54impl Digest {
55 pub fn genesis() -> Self {
57 Self::of(GENESIS.as_bytes())
58 }
59
60 pub fn of(bytes: &[u8]) -> Self {
61 Self(blake3::hash(bytes).into())
62 }
63}
64
65impl std::fmt::Display for Digest {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.write_str(blake3::Hash::from(self.0).to_hex().as_str())
68 }
69}
70
71impl std::fmt::Debug for Digest {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 write!(f, "{self}")
74 }
75}
76
77impl Serialize for Digest {
78 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
79 serializer.collect_str(self)
80 }
81}
82
83impl<'de> Deserialize<'de> for Digest {
84 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
85 let hex = String::deserialize(deserializer)?;
86 blake3::Hash::from_hex(&hex)
87 .map(|hash| Self(*hash.as_bytes()))
88 .map_err(serde::de::Error::custom)
89 }
90}
91
92#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
97pub struct Identity {
98 pub name: String,
99 #[serde(default)]
102 pub id: Option<String>,
103}
104
105impl Identity {
106 pub fn new(name: impl Into<String>) -> Self {
107 Self {
108 name: name.into(),
109 id: None,
110 }
111 }
112
113 pub fn from_environment() -> Self {
115 let name = std::env::var("USER")
116 .or_else(|_| std::env::var("USERNAME"))
117 .ok()
118 .filter(|name| !name.is_empty())
119 .unwrap_or_else(|| UNATTRIBUTED.to_owned());
120 Self::new(name)
121 }
122}
123
124#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
129#[serde(transparent)]
130pub struct ScopePath(Vec<BlockId>);
131
132impl ScopePath {
133 pub const ROOT: Self = Self(Vec::new());
135
136 pub fn segments(&self) -> &[BlockId] {
137 &self.0
138 }
139
140 pub fn is_empty(&self) -> bool {
142 self.0.is_empty()
143 }
144
145 pub fn innermost(&self) -> Option<BlockId> {
148 self.0.last().copied()
149 }
150}
151
152impl FromIterator<BlockId> for ScopePath {
153 fn from_iter<I: IntoIterator<Item = BlockId>>(ids: I) -> Self {
154 Self(ids.into_iter().collect())
155 }
156}
157
158#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
168pub struct Camera {
169 pub x: f32,
170 pub y: f32,
171 pub zoom: f32,
172}
173
174impl Camera {
175 pub const UNSEEN: Self = Self {
179 x: 0.0,
180 y: 0.0,
181 zoom: 1.0,
182 };
183}
184
185#[derive(Clone, Copy)]
190pub struct Standing<'a> {
191 path: &'a ScopePath,
192 names: &'a [String],
193}
194
195impl<'a> Standing<'a> {
196 pub const ROOT: Standing<'static> = Standing {
198 path: &ScopePath::ROOT,
199 names: &[],
200 };
201
202 pub fn new(path: &'a ScopePath, names: &'a [String]) -> Self {
205 debug_assert_eq!(
206 path.segments().len(),
207 names.len(),
208 "a scope's names must spell its own ids",
209 );
210 Self { path, names }
211 }
212
213 pub fn path(self) -> &'a ScopePath {
214 self.path
215 }
216
217 pub fn names(self) -> &'a [String] {
218 self.names
219 }
220}
221
222#[derive(Clone, Copy)]
228pub struct Attribution<'a> {
229 pub author: &'a Identity,
230 pub standing: Standing<'a>,
231 pub camera: Camera,
232}
233
234impl<'a> From<&'a Identity> for Attribution<'a> {
235 fn from(author: &'a Identity) -> Self {
236 Self {
237 author,
238 standing: Standing::ROOT,
239 camera: Camera::UNSEEN,
240 }
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn a_digest_survives_the_json_it_is_written_as() {
250 let digest = Digest::of(b"a row");
251 let text = serde_json::to_string(&digest).expect("a digest serializes");
252 assert_eq!(text, format!("\"{digest}\""), "hex text, not a byte array");
253 assert_eq!(
254 serde_json::from_str::<Digest>(&text).expect("and parses back"),
255 digest,
256 );
257 }
258
259 #[test]
260 fn a_digest_refuses_text_that_is_not_a_digest() {
261 assert!(serde_json::from_str::<Digest>("\"deadbeef\"").is_err());
262 }
263
264 #[test]
265 fn an_unnamed_environment_attributes_to_nobody_rather_than_to_an_empty_name() {
266 assert!(!Identity::from_environment().name.is_empty());
267 assert_eq!(Identity::new("ada").id, None);
268 }
269
270 #[test]
273 fn an_unplaced_attribution_stands_at_the_origin_at_unity() {
274 let author = Identity::new("ada");
275 let by = Attribution::from(&author);
276 assert!(by.standing.path().is_empty());
277 assert!(by.standing.names().is_empty());
278 assert_eq!(by.camera, Camera::UNSEEN);
279 }
280
281 #[test]
282 fn a_scope_path_names_the_block_its_camera_is_measured_in() {
283 let root = ScopePath::default();
284 assert!(root.is_empty());
285 assert_eq!(root.innermost(), None);
286
287 let inside: ScopePath = [
288 blockworx_doc::fixtures::block_id(1),
289 blockworx_doc::fixtures::block_id(4),
290 ]
291 .into_iter()
292 .collect();
293 assert_eq!(
294 inside.innermost(),
295 Some(blockworx_doc::fixtures::block_id(4))
296 );
297 assert!(!inside.is_empty());
298 }
299}