Skip to main content

axiolid_model/
id.rs

1//! Typed graph identity.
2
3use core::fmt;
4use core::sync::atomic::{AtomicU64, Ordering};
5
6static NEXT_GRAPH_ID: AtomicU64 = AtomicU64::new(1);
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
9pub(crate) struct GraphId(u64);
10
11impl GraphId {
12    pub(crate) fn fresh() -> Self {
13        let value = NEXT_GRAPH_ID
14            .try_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
15                current.checked_add(1)
16            })
17            .expect("geometry graph identity space exhausted");
18        Self(value)
19    }
20}
21
22/// Stable index owned by one immutable [`crate::GeometryGraph`].
23///
24/// A handle carries its graph's brand. Another builder refuses it
25/// ([`crate::GraphError::ForeignReference`]) and another graph's
26/// [`crate::GeometryGraph::get`] returns `None`, so a graph that references
27/// a node it does not own cannot be built at all rather than being detected
28/// later.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
30pub struct NodeId {
31    graph: GraphId,
32    index: u32,
33}
34
35impl NodeId {
36    pub(crate) fn from_index(graph: GraphId, index: usize) -> Self {
37        Self {
38            graph,
39            index: u32::try_from(index).expect("geometry graph exceeds u32 capacity"),
40        }
41    }
42
43    pub(crate) fn belongs_to(self, graph: GraphId) -> bool {
44        self.graph == graph
45    }
46
47    /// Zero-based graph index.
48    pub const fn index(self) -> usize {
49        self.index as usize
50    }
51}
52
53impl fmt::Display for NodeId {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        write!(f, "geometry#{}", self.index)
56    }
57}
58
59#[cfg(feature = "serde")]
60std::thread_local! {
61    static WIRE_BRAND: core::cell::Cell<Option<GraphId>> = const { core::cell::Cell::new(None) };
62}
63
64/// The brand node references take while a wire payload is read (ADR 0085).
65///
66/// A [`NodeId`] on the wire is only an index; its brand is the graph being
67/// rebuilt from the payload, set for the duration of the read by
68/// [`WireBrand::enter`]. Outside that scope there is no graph to brand a
69/// reference with, so deserialising a `NodeId` refuses rather than inventing
70/// one.
71#[cfg(feature = "serde")]
72pub(crate) struct WireBrand {
73    previous: Option<GraphId>,
74}
75
76#[cfg(feature = "serde")]
77impl WireBrand {
78    /// Brand every `NodeId` read on this thread with `graph` until the
79    /// returned guard drops.
80    pub(crate) fn enter(graph: GraphId) -> Self {
81        Self {
82            previous: WIRE_BRAND.with(|brand| brand.replace(Some(graph))),
83        }
84    }
85}
86
87#[cfg(feature = "serde")]
88impl Drop for WireBrand {
89    fn drop(&mut self) {
90        WIRE_BRAND.with(|brand| brand.set(self.previous));
91    }
92}
93
94/// A node reference is written as its zero-based index (ADR 0085).
95#[cfg(feature = "serde")]
96impl serde::Serialize for NodeId {
97    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
98        serializer.serialize_u32(self.index)
99    }
100}
101
102/// A node reference reads as an index into the graph payload being read.
103///
104/// It is refused outside a graph payload: a handle carries the brand of the
105/// graph that owns it, and only the graph reader knows that graph.
106#[cfg(feature = "serde")]
107impl<'de> serde::Deserialize<'de> for NodeId {
108    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
109        let index = <u32 as serde::Deserialize>::deserialize(deserializer)?;
110        match WIRE_BRAND.with(core::cell::Cell::get) {
111            Some(graph) => Ok(Self { graph, index }),
112            None => Err(serde::de::Error::custom(
113                "a node reference is only meaningful inside a geometry graph payload",
114            )),
115        }
116    }
117}