hwpforge_foundation/
index.rs1use std::fmt;
27use std::hash::{Hash, Hasher};
28use std::marker::PhantomData;
29
30use serde::{Deserialize, Deserializer, Serialize, Serializer};
31
32use crate::error::{FoundationError, FoundationResult};
33
34pub struct Index<T> {
55 value: usize,
56 _phantom: PhantomData<T>,
57}
58
59const _: () = assert!(std::mem::size_of::<Index<()>>() == std::mem::size_of::<usize>());
61
62impl<T> Clone for Index<T> {
65 fn clone(&self) -> Self {
66 *self
67 }
68}
69
70impl<T> Copy for Index<T> {}
71
72impl<T> PartialEq for Index<T> {
73 fn eq(&self, other: &Self) -> bool {
74 self.value == other.value
75 }
76}
77
78impl<T> Eq for Index<T> {}
79
80impl<T> PartialOrd for Index<T> {
81 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
82 Some(self.cmp(other))
83 }
84}
85
86impl<T> Ord for Index<T> {
87 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
88 self.value.cmp(&other.value)
89 }
90}
91
92impl<T> Hash for Index<T> {
93 fn hash<H: Hasher>(&self, state: &mut H) {
94 self.value.hash(state);
95 }
96}
97
98impl<T> Index<T> {
99 pub const fn new(value: usize) -> Self {
104 Self { value, _phantom: PhantomData }
105 }
106
107 pub const fn get(self) -> usize {
115 self.value
116 }
117
118 pub fn checked_get(self, max: usize) -> FoundationResult<usize> {
135 if self.value >= max {
136 return Err(FoundationError::IndexOutOfBounds {
137 index: self.value,
138 max,
139 type_name: std::any::type_name::<T>(),
140 });
141 }
142 Ok(self.value)
143 }
144}
145
146impl<T> fmt::Debug for Index<T> {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 let full = std::any::type_name::<T>();
150 let short = full.rsplit("::").next().unwrap_or(full);
151 write!(f, "Index<{short}>({})", self.value)
152 }
153}
154
155impl<T> fmt::Display for Index<T> {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 let full = std::any::type_name::<T>();
158 let short = full.rsplit("::").next().unwrap_or(full);
159 write!(f, "{short}[{}]", self.value)
160 }
161}
162
163impl<T> Serialize for Index<T> {
165 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
166 self.value.serialize(serializer)
167 }
168}
169
170impl<'de, T> Deserialize<'de> for Index<T> {
171 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
172 let value = usize::deserialize(deserializer)?;
173 Ok(Self::new(value))
174 }
175}
176
177impl<T> schemars::JsonSchema for Index<T> {
178 fn schema_name() -> std::borrow::Cow<'static, str> {
179 "Index".into()
180 }
181
182 fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
183 gen.subschema_for::<usize>()
184 }
185}
186
187pub struct CharShapeMarker;
193pub struct ParaShapeMarker;
195pub struct FontMarker;
197pub struct BorderFillMarker;
199pub struct StyleMarker;
201pub struct NumberingMarker;
203pub struct BulletMarker;
205pub struct TabMarker;
207
208pub type CharShapeIndex = Index<CharShapeMarker>;
210pub type ParaShapeIndex = Index<ParaShapeMarker>;
212pub type FontIndex = Index<FontMarker>;
214pub type BorderFillIndex = Index<BorderFillMarker>;
216pub type StyleIndex = Index<StyleMarker>;
218pub type NumberingIndex = Index<NumberingMarker>;
220pub type BulletIndex = Index<BulletMarker>;
222pub type TabIndex = Index<TabMarker>;
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
235 fn index_zero_is_valid() {
236 let idx = CharShapeIndex::new(0);
237 assert_eq!(idx.get(), 0);
238 assert!(idx.checked_get(1).is_ok());
239 }
240
241 #[test]
243 fn index_in_range() {
244 let idx = CharShapeIndex::new(5);
245 assert_eq!(idx.checked_get(10).unwrap(), 5);
246 }
247
248 #[test]
250 fn index_out_of_range() {
251 let idx = CharShapeIndex::new(10);
252 let err = idx.checked_get(5).unwrap_err();
253 match err {
254 FoundationError::IndexOutOfBounds { index, max, type_name } => {
255 assert_eq!(index, 10);
256 assert_eq!(max, 5);
257 assert!(type_name.contains("CharShape"), "type_name: {type_name}");
258 }
259 other => panic!("unexpected error: {other}"),
260 }
261 }
262
263 #[test]
265 fn index_at_exact_boundary_is_error() {
266 let idx = CharShapeIndex::new(5);
267 assert!(idx.checked_get(5).is_err());
268 }
269
270 #[test]
272 fn index_just_below_boundary() {
273 let idx = CharShapeIndex::new(4);
274 assert_eq!(idx.checked_get(5).unwrap(), 4);
275 }
276
277 #[test]
279 fn index_usize_max() {
280 let idx = CharShapeIndex::new(usize::MAX);
281 assert_eq!(idx.get(), usize::MAX);
282 assert!(idx.checked_get(usize::MAX).is_err()); }
284
285 #[test]
287 fn index_type_safety() {
288 fn accept_char_shape(_: CharShapeIndex) {}
289 fn accept_para_shape(_: ParaShapeIndex) {}
290
291 let cs = CharShapeIndex::new(0);
292 let ps = ParaShapeIndex::new(0);
293
294 accept_char_shape(cs);
295 accept_para_shape(ps);
296 }
298
299 #[test]
301 fn index_equality() {
302 let a = CharShapeIndex::new(5);
303 let b = CharShapeIndex::new(5);
304 let c = CharShapeIndex::new(6);
305 assert_eq!(a, b);
306 assert_ne!(a, c);
307 }
308
309 #[test]
311 fn index_hash() {
312 use std::collections::HashMap;
313 let mut map = HashMap::new();
314 map.insert(FontIndex::new(0), "Batang");
315 map.insert(FontIndex::new(1), "Dotum");
316 assert_eq!(map[&FontIndex::new(0)], "Batang");
317 }
318
319 #[test]
321 fn index_ord() {
322 let a = CharShapeIndex::new(3);
323 let b = CharShapeIndex::new(7);
324 assert!(a < b);
325 }
326
327 #[test]
329 fn index_display() {
330 let idx = CharShapeIndex::new(3);
331 let s = idx.to_string();
332 assert!(s.contains("CharShape"), "display: {s}");
333 assert!(s.contains("[3]"), "display: {s}");
334 }
335
336 #[test]
338 fn index_debug() {
339 let idx = FontIndex::new(42);
340 let s = format!("{idx:?}");
341 assert!(s.contains("Font"), "debug: {s}");
342 assert!(s.contains("42"), "debug: {s}");
343 }
344
345 #[test]
347 fn index_serde_as_usize() {
348 let idx = CharShapeIndex::new(7);
349 let json = serde_json::to_string(&idx).unwrap();
350 assert_eq!(json, "7");
351 let back: CharShapeIndex = serde_json::from_str(&json).unwrap();
352 assert_eq!(back, idx);
353 }
354
355 #[test]
357 fn index_is_copy() {
358 let a = CharShapeIndex::new(1);
359 let b = a; assert_eq!(a, b); }
362
363 #[test]
365 fn index_checked_get_empty_collection() {
366 let idx = CharShapeIndex::new(0);
367 assert!(idx.checked_get(0).is_err());
368 }
369
370 use proptest::prelude::*;
375
376 proptest! {
377 #[test]
378 fn prop_index_in_bounds(idx in 0usize..1000, max in 1usize..2000) {
379 let index = CharShapeIndex::new(idx);
380 if idx < max {
381 prop_assert_eq!(index.checked_get(max).unwrap(), idx);
382 } else {
383 prop_assert!(index.checked_get(max).is_err());
384 }
385 }
386
387 #[test]
388 fn prop_index_serde_roundtrip(val in 0usize..100_000) {
389 let idx = FontIndex::new(val);
390 let json = serde_json::to_string(&idx).unwrap();
391 let back: FontIndex = serde_json::from_str(&json).unwrap();
392 prop_assert_eq!(idx, back);
393 }
394 }
395}