Skip to main content

hwpforge_core/
object_id.rs

1//! Stable document-object identity used to link cross-references to targets.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7/// A document-object identity that links a cross-reference to its target.
8///
9/// Cross-reference *targets* — image, table, equation, group, text-art,
10/// footnote, endnote — carry an `Option<ObjectId>` (the `inst_id` field),
11/// and the *referencing* [`Control::CrossRef`](crate::Control::CrossRef)
12/// carries the **same** `ObjectId` via
13/// [`RefTarget::Object`](crate::control::RefTarget::Object). Sharing one id
14/// type makes the target↔referencer link checkable at the type level instead
15/// of relying on two unrelated integer fields coincidentally agreeing on a
16/// raw value (the latent fragility this newtype replaces — see ADR-010).
17///
18/// `ObjectId` is format-agnostic: Core only carries *identity*. The wire
19/// encoder owns id *allocation* — preserving ids imported from a source
20/// document and assigning fresh ids to authored objects. Serialization is
21/// [`#[serde(transparent)]`](https://serde.rs/container-attrs.html#transparent),
22/// so an `ObjectId` round-trips through JSON/YAML as a bare integer.
23#[derive(
24    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
25)]
26#[serde(transparent)]
27pub struct ObjectId(u64);
28
29impl ObjectId {
30    /// Wraps a raw integer id.
31    pub const fn new(value: u64) -> Self {
32        Self(value)
33    }
34
35    /// Returns the wrapped raw integer id.
36    pub const fn value(self) -> u64 {
37        self.0
38    }
39}
40
41impl fmt::Display for ObjectId {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        write!(f, "{}", self.0)
44    }
45}
46
47impl From<u64> for ObjectId {
48    fn from(value: u64) -> Self {
49        Self(value)
50    }
51}
52
53impl From<ObjectId> for u64 {
54    fn from(id: ObjectId) -> Self {
55        id.0
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn new_and_value_round_trip() {
65        assert_eq!(ObjectId::new(0).value(), 0);
66        assert_eq!(ObjectId::new(u64::MAX).value(), u64::MAX);
67        assert_eq!(ObjectId::new(1_108_165_575).value(), 1_108_165_575);
68    }
69
70    #[test]
71    fn display_is_bare_integer() {
72        assert_eq!(ObjectId::new(42).to_string(), "42");
73        assert_eq!(ObjectId::new(0).to_string(), "0");
74    }
75
76    #[test]
77    fn from_conversions() {
78        let id: ObjectId = 7u64.into();
79        assert_eq!(id, ObjectId::new(7));
80        let raw: u64 = ObjectId::new(7).into();
81        assert_eq!(raw, 7);
82    }
83
84    #[test]
85    fn serde_is_transparent_bare_integer() {
86        let id = ObjectId::new(40_257_166);
87        let json = serde_json::to_string(&id).unwrap();
88        assert_eq!(json, "40257166");
89        let back: ObjectId = serde_json::from_str(&json).unwrap();
90        assert_eq!(back, id);
91
92        // Round-trips inside an Option exactly like a bare integer would.
93        let some: Option<ObjectId> = Some(id);
94        assert_eq!(serde_json::to_string(&some).unwrap(), "40257166");
95        let none: Option<ObjectId> = None;
96        assert_eq!(serde_json::to_string(&none).unwrap(), "null");
97    }
98
99    #[test]
100    fn equality_enables_link_check() {
101        // The whole point: a target id and a referencer id are comparable.
102        let target = Some(ObjectId::new(123));
103        let referencer = ObjectId::new(123);
104        assert_eq!(target, Some(referencer));
105    }
106}