1use blake3;
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeMap;
12use std::fmt;
13
14#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
17pub struct ObjectId([u8; 32]);
18
19impl ObjectId {
20 pub fn from_bytes(bytes: [u8; 32]) -> Self {
22 Self(bytes)
23 }
24
25 pub fn as_bytes(&self) -> &[u8; 32] {
27 &self.0
28 }
29
30 pub fn hash(data: &[u8]) -> Self {
32 Self(*blake3::hash(data).as_bytes())
33 }
34
35 pub fn short(&self) -> String {
37 format!("sg_{}", to_hex(&self.0[..6]))
38 }
39
40 pub fn to_hex(&self) -> String {
43 to_hex(&self.0)
44 }
45
46 pub fn from_hex(s: &str) -> Option<Self> {
51 let hex = s.trim().strip_prefix("sg_").unwrap_or(s.trim());
52 if hex.len() != 64 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
53 return None;
54 }
55 let mut bytes = [0u8; 32];
56 for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
57 let hi = (chunk[0] as char).to_digit(16)?;
58 let lo = (chunk[1] as char).to_digit(16)?;
59 bytes[i] = (hi * 16 + lo) as u8;
60 }
61 Some(Self(bytes))
62 }
63
64 pub fn normalize_hex_prefix(s: &str) -> Option<String> {
68 let frag = s.trim().strip_prefix("sg_").unwrap_or(s.trim());
69 if frag.is_empty() || !frag.bytes().all(|b| b.is_ascii_hexdigit()) {
70 return None;
71 }
72 Some(frag.to_ascii_lowercase())
73 }
74}
75
76impl fmt::Display for ObjectId {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 write!(f, "sg_{}", to_hex(&self.0))
79 }
80}
81
82fn to_hex(bytes: &[u8]) -> String {
84 bytes.iter().map(|b| format!("{:02x}", b)).collect()
85}
86
87impl fmt::Debug for ObjectId {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 write!(f, "ObjectId({})", self.short())
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95#[serde(tag = "type", content = "value")]
96pub enum Atom {
97 Null,
98 Bool(bool),
99 Int(i64),
100 Float(f64),
101 String(String),
102 Bytes(Vec<u8>),
103 UInt(u64),
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
114#[serde(tag = "type", content = "value")]
115pub enum Node {
116 Map(BTreeMap<String, ObjectId>),
118 List(Vec<ObjectId>),
120 Set(Vec<ObjectId>),
122}
123
124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
127#[serde(tag = "kind")]
128pub enum Object {
129 Atom(Atom),
130 Node(Node),
131}
132
133impl Object {
134 pub fn canonical_bytes(&self) -> Vec<u8> {
138 serde_json::to_vec(self).expect("Object serialization should never fail")
140 }
141
142 pub fn id(&self) -> ObjectId {
144 ObjectId::hash(&self.canonical_bytes())
145 }
146
147 pub fn null() -> Self {
150 Object::Atom(Atom::Null)
151 }
152
153 pub fn bool(v: bool) -> Self {
154 Object::Atom(Atom::Bool(v))
155 }
156
157 pub fn int(v: i64) -> Self {
158 Object::Atom(Atom::Int(v))
159 }
160
161 pub fn float(v: f64) -> Self {
162 Object::Atom(Atom::Float(v))
163 }
164
165 pub fn uint(v: u64) -> Self {
167 match i64::try_from(v) {
168 Ok(i) => Object::Atom(Atom::Int(i)),
169 Err(_) => Object::Atom(Atom::UInt(v)),
170 }
171 }
172
173 pub fn from_json_number(n: &serde_json::Number) -> Self {
178 if let Some(i) = n.as_i64() {
179 Object::int(i)
180 } else if let Some(u) = n.as_u64() {
181 Object::uint(u)
182 } else {
183 Object::float(n.as_f64().unwrap_or(f64::NAN))
184 }
185 }
186
187 pub fn string(v: impl Into<String>) -> Self {
188 Object::Atom(Atom::String(v.into()))
189 }
190
191 pub fn bytes(v: Vec<u8>) -> Self {
192 Object::Atom(Atom::Bytes(v))
193 }
194
195 pub fn map(entries: BTreeMap<String, ObjectId>) -> Self {
196 Object::Node(Node::Map(entries))
197 }
198
199 pub fn empty_map() -> Self {
200 Object::Node(Node::Map(BTreeMap::new()))
201 }
202
203 pub fn list(items: Vec<ObjectId>) -> Self {
204 Object::Node(Node::List(items))
205 }
206
207 pub fn set(items: Vec<ObjectId>) -> Self {
208 let mut sorted = items;
209 sorted.sort();
210 sorted.dedup();
211 Object::Node(Node::Set(sorted))
212 }
213}
214
215#[cfg(test)]
216mod tests {
217
218 #[test]
219 fn integers_have_one_representation() {
220 assert_eq!(Object::uint(5), Object::int(5));
221 assert_eq!(Object::uint(i64::MAX as u64), Object::int(i64::MAX));
222 assert_eq!(Object::uint(u64::MAX), Object::Atom(Atom::UInt(u64::MAX)));
223 let n = |v: serde_json::Value| match v {
224 serde_json::Value::Number(n) => Object::from_json_number(&n),
225 _ => unreachable!(),
226 };
227 assert_eq!(n(serde_json::json!(5u64)), Object::int(5));
228 assert_eq!(n(serde_json::json!(u64::MAX)), Object::uint(u64::MAX));
229 assert_eq!(n(serde_json::json!(-3)), Object::int(-3));
230 assert_eq!(n(serde_json::json!(0.5)), Object::float(0.5));
231 }
232
233 #[test]
234 fn existing_atoms_serialize_unchanged() {
235 assert_eq!(
237 String::from_utf8(Object::int(42).canonical_bytes()).unwrap(),
238 r#"{"kind":"Atom","type":"Int","value":42}"#
239 );
240 assert_eq!(
241 String::from_utf8(Object::uint(u64::MAX).canonical_bytes()).unwrap(),
242 r#"{"kind":"Atom","type":"UInt","value":18446744073709551615}"#
243 );
244 }
245
246 use super::*;
247
248 #[test]
249 fn test_content_addressing_deterministic() {
250 let obj1 = Object::string("hello");
251 let obj2 = Object::string("hello");
252 assert_eq!(
253 obj1.id(),
254 obj2.id(),
255 "identical objects must have the same ObjectId"
256 );
257 }
258
259 #[test]
260 fn test_different_content_different_id() {
261 let obj1 = Object::string("hello");
262 let obj2 = Object::string("world");
263 assert_ne!(
264 obj1.id(),
265 obj2.id(),
266 "different objects must have different ObjectIds"
267 );
268 }
269
270 #[test]
271 fn test_map_key_order_irrelevant() {
272 let id_a = Object::string("a").id();
274 let id_b = Object::string("b").id();
275
276 let mut map1 = BTreeMap::new();
277 map1.insert("first".to_string(), id_a);
278 map1.insert("second".to_string(), id_b);
279
280 let mut map2 = BTreeMap::new();
281 map2.insert("second".to_string(), id_b);
282 map2.insert("first".to_string(), id_a);
283
284 let obj1 = Object::map(map1);
285 let obj2 = Object::map(map2);
286 assert_eq!(
287 obj1.id(),
288 obj2.id(),
289 "maps with same entries in different order must have same ObjectId"
290 );
291 }
292
293 #[test]
294 fn test_set_dedup_and_sort() {
295 let id_a = Object::string("a").id();
296 let id_b = Object::string("b").id();
297
298 let obj1 = Object::set(vec![id_a, id_b, id_a]); let obj2 = Object::set(vec![id_b, id_a]); assert_eq!(obj1.id(), obj2.id(), "sets must be deduplicated and sorted");
302 }
303
304 #[test]
305 fn test_object_id_display() {
306 let obj = Object::string("test");
307 let id = obj.id();
308 let display = format!("{}", id);
309 assert!(
310 display.starts_with("sg_"),
311 "ObjectId display should start with sg_ prefix"
312 );
313 }
314
315 #[test]
316 fn test_atom_variants() {
317 let ids: Vec<ObjectId> = vec![
319 Object::null().id(),
320 Object::bool(true).id(),
321 Object::bool(false).id(),
322 Object::int(42).id(),
323 Object::float(std::f64::consts::PI).id(),
324 Object::string("test").id(),
325 Object::bytes(vec![1, 2, 3]).id(),
326 ];
327
328 for i in 0..ids.len() {
329 for j in (i + 1)..ids.len() {
330 assert_ne!(
331 ids[i], ids[j],
332 "different atom types/values must have different ObjectIds"
333 );
334 }
335 }
336 }
337}