Skip to main content

cityjson_types/resources/
handles.rs

1//! # Opaque resource handle types
2//!
3//! Type-safe opaque handles for different resource types.
4//! Each handle wraps a `ResourceId32` internally.
5//! Handles use `#[repr(transparent)]` for zero runtime overhead.
6//! Apart from the `Default` implementation, handles cannot be created directly,
7//! to enforce the integrity of the `ResourcePool`. Thus, all handles are created by
8//! the `ResourcePool`.
9//!
10//! Trusted serializers can persist and restore handle identities through
11//! [`GeometryHandle::raw_parts`] and the corresponding `*_unchecked` reconstruction methods, but
12//! ordinary callers should continue to obtain handles from the owning pools.
13//!
14//! # Examples
15//!
16//! ```
17//! use cityjson_types::resources::handles::{GeometryHandle, MaterialHandle};
18//!
19//! let geometry = GeometryHandle::default();
20//! let material = MaterialHandle::default();
21//!
22//! assert!(geometry.is_null());
23//! assert!(material.is_null());
24//! assert_eq!(format!("{geometry}"), "GeometryHandle");
25//! assert_eq!(format!("{material:?}"), "MaterialHandle(index=0, generation=0)");
26//! ```
27
28use crate::resources::id::ResourceId32;
29use std::fmt::{Display, Formatter};
30use std::hash::Hash;
31
32/// Internal trait for converting between typed handles and raw `ResourceId32` values.
33#[allow(dead_code)]
34pub(crate) trait HandleType: Copy + Clone + PartialEq + Eq + Hash + Default {
35    fn from_raw(raw: ResourceId32) -> Self;
36    fn to_raw(self) -> ResourceId32;
37
38    fn is_null(self) -> bool {
39        let raw = self.to_raw();
40        raw.index() == 0 && raw.generation() == 0
41    }
42}
43
44/// Macro to define a newtype handle around `ResourceId32`.
45macro_rules! define_handle {
46    (
47        $(#[$meta:meta])*
48        $name:ident
49    ) => {
50        $(#[$meta])*
51        #[repr(transparent)]
52        #[derive(Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
53        pub struct $name(ResourceId32);
54
55        #[allow(dead_code)]
56        impl $name {
57            #[must_use]
58            pub fn is_null(self) -> bool {
59                self.0.index() == 0 && self.0.generation() == 0
60            }
61
62            pub(crate) fn from_parts(index: u32, generation: u16) -> Self {
63                Self(ResourceId32::new(index, generation))
64            }
65
66            pub(crate) fn index(self) -> u32 {
67                self.0.index()
68            }
69
70            pub(crate) fn generation(self) -> u16 {
71                self.0.generation()
72            }
73
74            /// Returns the raw `(slot, generation)` representation used by the owning pool.
75            ///
76            /// This is intended for trusted serialization code that needs to persist handle
77            /// identity without introducing a separate forward-map layer.
78            #[must_use]
79            pub fn raw_parts(self) -> (u32, u16) {
80                self.to_raw_parts()
81            }
82
83            /// Reconstructs a handle from trusted raw `(slot, generation)` parts.
84            ///
85            /// Normal callers should not create handles directly; obtain them from the owning
86            /// resource pool instead.
87            ///
88            /// # Safety
89            ///
90            /// `index` and `generation` must come from a compatible serialized handle identity for
91            /// the same resource pool domain. Constructing arbitrary values is not memory-unsafe,
92            /// but it can create stale or invalid handles that fail pool validity checks or point
93            /// at the wrong logical resource.
94            #[must_use]
95            pub unsafe fn from_raw_parts_unchecked(index: u32, generation: u16) -> Self {
96                Self::from_raw_parts(index, generation)
97            }
98
99            pub(crate) fn from_raw_parts(index: u32, generation: u16) -> Self {
100                Self(ResourceId32::new(index, generation))
101            }
102
103            pub(crate) fn to_raw_parts(self) -> (u32, u16) {
104                (self.0.index(), self.0.generation())
105            }
106
107            pub(crate) fn to_raw(self) -> ResourceId32 {
108                self.0
109            }
110
111            pub(crate) fn from_raw(raw: ResourceId32) -> Self {
112                Self(raw)
113            }
114        }
115
116        impl Display for $name {
117            fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
118                write!(f, "{}", stringify!($name))
119            }
120        }
121
122        impl std::fmt::Debug for $name {
123            fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
124                write!(f, "{}(index={}, generation={})", stringify!($name), self.0.index(), self.0.generation())
125            }
126        }
127
128        impl HandleType for $name {
129            fn from_raw(raw: ResourceId32) -> Self {
130                Self(raw)
131            }
132
133            fn to_raw(self) -> ResourceId32 {
134                self.0
135            }
136        }
137    };
138}
139
140define_handle! {
141    /// Handle to a Geometry.
142    GeometryHandle
143}
144
145define_handle! {
146    /// Handle to a `GeometryTemplate`.
147    GeometryTemplateHandle
148}
149
150define_handle! {
151    /// Handle to a Semantic.
152    SemanticHandle
153}
154
155define_handle! {
156    /// Handle to a Material.
157    MaterialHandle
158}
159
160define_handle! {
161    /// Handle to a Texture.
162    TextureHandle
163}
164
165define_handle! {
166    /// Handle to a `CityObject`.
167    CityObjectHandle
168}
169
170#[inline]
171pub(crate) fn cast_handle_slice<H: HandleType>(raw: &[ResourceId32]) -> &[H] {
172    const {
173        assert!(std::mem::size_of::<H>() == std::mem::size_of::<ResourceId32>());
174        assert!(std::mem::align_of::<H>() == std::mem::align_of::<ResourceId32>());
175    }
176
177    // SAFETY: all exported handle types are `#[repr(transparent)]` wrappers over `ResourceId32`,
178    // and compile-time layout assertions above guarantee identical size/alignment.
179    unsafe { std::slice::from_raw_parts(raw.as_ptr().cast::<H>(), raw.len()) }
180}
181
182#[inline]
183pub(crate) fn cast_option_handle_slice<H: HandleType>(
184    raw: &[Option<ResourceId32>],
185) -> &[Option<H>] {
186    const {
187        assert!(std::mem::size_of::<Option<H>>() == std::mem::size_of::<Option<ResourceId32>>());
188        assert!(std::mem::align_of::<Option<H>>() == std::mem::align_of::<Option<ResourceId32>>());
189    }
190
191    // SAFETY: handle types are `#[repr(transparent)]` wrappers over `ResourceId32`, and
192    // `Option<Handle>` has identical layout to `Option<ResourceId32>` due compile-time checks.
193    unsafe { std::slice::from_raw_parts(raw.as_ptr().cast::<Option<H>>(), raw.len()) }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::{GeometryHandle, MaterialHandle};
199
200    #[test]
201    fn raw_parts_roundtrip_preserves_handle_identity() {
202        let handle = GeometryHandle::from_parts(42, 7);
203
204        assert_eq!(handle.raw_parts(), (42, 7));
205
206        let rebuilt = unsafe { GeometryHandle::from_raw_parts_unchecked(42, 7) };
207        assert_eq!(rebuilt, handle);
208        assert_eq!(rebuilt.raw_parts(), (42, 7));
209    }
210
211    #[test]
212    fn raw_parts_roundtrip_preserves_null_handles() {
213        let handle = MaterialHandle::default();
214        assert!(handle.is_null());
215        assert_eq!(handle.raw_parts(), (0, 0));
216
217        let rebuilt = unsafe { MaterialHandle::from_raw_parts_unchecked(0, 0) };
218        assert!(rebuilt.is_null());
219        assert_eq!(rebuilt.raw_parts(), (0, 0));
220    }
221}