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 TabMarker;
205
206pub type CharShapeIndex = Index<CharShapeMarker>;
208pub type ParaShapeIndex = Index<ParaShapeMarker>;
210pub type FontIndex = Index<FontMarker>;
212pub type BorderFillIndex = Index<BorderFillMarker>;
214pub type StyleIndex = Index<StyleMarker>;
216pub type NumberingIndex = Index<NumberingMarker>;
218pub type TabIndex = Index<TabMarker>;
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
231 fn index_zero_is_valid() {
232 let idx = CharShapeIndex::new(0);
233 assert_eq!(idx.get(), 0);
234 assert!(idx.checked_get(1).is_ok());
235 }
236
237 #[test]
239 fn index_in_range() {
240 let idx = CharShapeIndex::new(5);
241 assert_eq!(idx.checked_get(10).unwrap(), 5);
242 }
243
244 #[test]
246 fn index_out_of_range() {
247 let idx = CharShapeIndex::new(10);
248 let err = idx.checked_get(5).unwrap_err();
249 match err {
250 FoundationError::IndexOutOfBounds { index, max, type_name } => {
251 assert_eq!(index, 10);
252 assert_eq!(max, 5);
253 assert!(type_name.contains("CharShape"), "type_name: {type_name}");
254 }
255 other => panic!("unexpected error: {other}"),
256 }
257 }
258
259 #[test]
261 fn index_at_exact_boundary_is_error() {
262 let idx = CharShapeIndex::new(5);
263 assert!(idx.checked_get(5).is_err());
264 }
265
266 #[test]
268 fn index_just_below_boundary() {
269 let idx = CharShapeIndex::new(4);
270 assert_eq!(idx.checked_get(5).unwrap(), 4);
271 }
272
273 #[test]
275 fn index_usize_max() {
276 let idx = CharShapeIndex::new(usize::MAX);
277 assert_eq!(idx.get(), usize::MAX);
278 assert!(idx.checked_get(usize::MAX).is_err()); }
280
281 #[test]
283 fn index_type_safety() {
284 fn accept_char_shape(_: CharShapeIndex) {}
285 fn accept_para_shape(_: ParaShapeIndex) {}
286
287 let cs = CharShapeIndex::new(0);
288 let ps = ParaShapeIndex::new(0);
289
290 accept_char_shape(cs);
291 accept_para_shape(ps);
292 }
294
295 #[test]
297 fn index_equality() {
298 let a = CharShapeIndex::new(5);
299 let b = CharShapeIndex::new(5);
300 let c = CharShapeIndex::new(6);
301 assert_eq!(a, b);
302 assert_ne!(a, c);
303 }
304
305 #[test]
307 fn index_hash() {
308 use std::collections::HashMap;
309 let mut map = HashMap::new();
310 map.insert(FontIndex::new(0), "Batang");
311 map.insert(FontIndex::new(1), "Dotum");
312 assert_eq!(map[&FontIndex::new(0)], "Batang");
313 }
314
315 #[test]
317 fn index_ord() {
318 let a = CharShapeIndex::new(3);
319 let b = CharShapeIndex::new(7);
320 assert!(a < b);
321 }
322
323 #[test]
325 fn index_display() {
326 let idx = CharShapeIndex::new(3);
327 let s = idx.to_string();
328 assert!(s.contains("CharShape"), "display: {s}");
329 assert!(s.contains("[3]"), "display: {s}");
330 }
331
332 #[test]
334 fn index_debug() {
335 let idx = FontIndex::new(42);
336 let s = format!("{idx:?}");
337 assert!(s.contains("Font"), "debug: {s}");
338 assert!(s.contains("42"), "debug: {s}");
339 }
340
341 #[test]
343 fn index_serde_as_usize() {
344 let idx = CharShapeIndex::new(7);
345 let json = serde_json::to_string(&idx).unwrap();
346 assert_eq!(json, "7");
347 let back: CharShapeIndex = serde_json::from_str(&json).unwrap();
348 assert_eq!(back, idx);
349 }
350
351 #[test]
353 fn index_is_copy() {
354 let a = CharShapeIndex::new(1);
355 let b = a; assert_eq!(a, b); }
358
359 #[test]
361 fn index_checked_get_empty_collection() {
362 let idx = CharShapeIndex::new(0);
363 assert!(idx.checked_get(0).is_err());
364 }
365
366 use proptest::prelude::*;
371
372 proptest! {
373 #[test]
374 fn prop_index_in_bounds(idx in 0usize..1000, max in 1usize..2000) {
375 let index = CharShapeIndex::new(idx);
376 if idx < max {
377 prop_assert_eq!(index.checked_get(max).unwrap(), idx);
378 } else {
379 prop_assert!(index.checked_get(max).is_err());
380 }
381 }
382
383 #[test]
384 fn prop_index_serde_roundtrip(val in 0usize..100_000) {
385 let idx = FontIndex::new(val);
386 let json = serde_json::to_string(&idx).unwrap();
387 let back: FontIndex = serde_json::from_str(&json).unwrap();
388 prop_assert_eq!(idx, back);
389 }
390 }
391}