use std::fmt;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::error::{FoundationError, FoundationResult};
pub struct Index<T> {
value: usize,
_phantom: PhantomData<T>,
}
const _: () = assert!(std::mem::size_of::<Index<()>>() == std::mem::size_of::<usize>());
impl<T> Clone for Index<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Index<T> {}
impl<T> PartialEq for Index<T> {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
}
impl<T> Eq for Index<T> {}
impl<T> PartialOrd for Index<T> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<T> Ord for Index<T> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.value.cmp(&other.value)
}
}
impl<T> Hash for Index<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.value.hash(state);
}
}
impl<T> Index<T> {
pub const fn new(value: usize) -> Self {
Self { value, _phantom: PhantomData }
}
pub const fn get(self) -> usize {
self.value
}
pub fn checked_get(self, max: usize) -> FoundationResult<usize> {
if self.value >= max {
return Err(FoundationError::IndexOutOfBounds {
index: self.value,
max,
type_name: std::any::type_name::<T>(),
});
}
Ok(self.value)
}
}
impl<T> fmt::Debug for Index<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let full = std::any::type_name::<T>();
let short = full.rsplit("::").next().unwrap_or(full);
write!(f, "Index<{short}>({})", self.value)
}
}
impl<T> fmt::Display for Index<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let full = std::any::type_name::<T>();
let short = full.rsplit("::").next().unwrap_or(full);
write!(f, "{short}[{}]", self.value)
}
}
impl<T> Serialize for Index<T> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.value.serialize(serializer)
}
}
impl<'de, T> Deserialize<'de> for Index<T> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = usize::deserialize(deserializer)?;
Ok(Self::new(value))
}
}
impl<T> schemars::JsonSchema for Index<T> {
fn schema_name() -> std::borrow::Cow<'static, str> {
"Index".into()
}
fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
gen.subschema_for::<usize>()
}
}
pub struct CharShapeMarker;
pub struct ParaShapeMarker;
pub struct FontMarker;
pub struct BorderFillMarker;
pub struct StyleMarker;
pub struct NumberingMarker;
pub struct BulletMarker;
pub struct TabMarker;
pub type CharShapeIndex = Index<CharShapeMarker>;
pub type ParaShapeIndex = Index<ParaShapeMarker>;
pub type FontIndex = Index<FontMarker>;
pub type BorderFillIndex = Index<BorderFillMarker>;
pub type StyleIndex = Index<StyleMarker>;
pub type NumberingIndex = Index<NumberingMarker>;
pub type BulletIndex = Index<BulletMarker>;
pub type TabIndex = Index<TabMarker>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn index_zero_is_valid() {
let idx = CharShapeIndex::new(0);
assert_eq!(idx.get(), 0);
assert!(idx.checked_get(1).is_ok());
}
#[test]
fn index_in_range() {
let idx = CharShapeIndex::new(5);
assert_eq!(idx.checked_get(10).unwrap(), 5);
}
#[test]
fn index_out_of_range() {
let idx = CharShapeIndex::new(10);
let err = idx.checked_get(5).unwrap_err();
match err {
FoundationError::IndexOutOfBounds { index, max, type_name } => {
assert_eq!(index, 10);
assert_eq!(max, 5);
assert!(type_name.contains("CharShape"), "type_name: {type_name}");
}
other => panic!("unexpected error: {other}"),
}
}
#[test]
fn index_at_exact_boundary_is_error() {
let idx = CharShapeIndex::new(5);
assert!(idx.checked_get(5).is_err());
}
#[test]
fn index_just_below_boundary() {
let idx = CharShapeIndex::new(4);
assert_eq!(idx.checked_get(5).unwrap(), 4);
}
#[test]
fn index_usize_max() {
let idx = CharShapeIndex::new(usize::MAX);
assert_eq!(idx.get(), usize::MAX);
assert!(idx.checked_get(usize::MAX).is_err()); }
#[test]
fn index_type_safety() {
fn accept_char_shape(_: CharShapeIndex) {}
fn accept_para_shape(_: ParaShapeIndex) {}
let cs = CharShapeIndex::new(0);
let ps = ParaShapeIndex::new(0);
accept_char_shape(cs);
accept_para_shape(ps);
}
#[test]
fn index_equality() {
let a = CharShapeIndex::new(5);
let b = CharShapeIndex::new(5);
let c = CharShapeIndex::new(6);
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn index_hash() {
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert(FontIndex::new(0), "Batang");
map.insert(FontIndex::new(1), "Dotum");
assert_eq!(map[&FontIndex::new(0)], "Batang");
}
#[test]
fn index_ord() {
let a = CharShapeIndex::new(3);
let b = CharShapeIndex::new(7);
assert!(a < b);
}
#[test]
fn index_display() {
let idx = CharShapeIndex::new(3);
let s = idx.to_string();
assert!(s.contains("CharShape"), "display: {s}");
assert!(s.contains("[3]"), "display: {s}");
}
#[test]
fn index_debug() {
let idx = FontIndex::new(42);
let s = format!("{idx:?}");
assert!(s.contains("Font"), "debug: {s}");
assert!(s.contains("42"), "debug: {s}");
}
#[test]
fn index_serde_as_usize() {
let idx = CharShapeIndex::new(7);
let json = serde_json::to_string(&idx).unwrap();
assert_eq!(json, "7");
let back: CharShapeIndex = serde_json::from_str(&json).unwrap();
assert_eq!(back, idx);
}
#[test]
fn index_is_copy() {
let a = CharShapeIndex::new(1);
let b = a; assert_eq!(a, b); }
#[test]
fn index_checked_get_empty_collection() {
let idx = CharShapeIndex::new(0);
assert!(idx.checked_get(0).is_err());
}
use proptest::prelude::*;
proptest! {
#[test]
fn prop_index_in_bounds(idx in 0usize..1000, max in 1usize..2000) {
let index = CharShapeIndex::new(idx);
if idx < max {
prop_assert_eq!(index.checked_get(max).unwrap(), idx);
} else {
prop_assert!(index.checked_get(max).is_err());
}
}
#[test]
fn prop_index_serde_roundtrip(val in 0usize..100_000) {
let idx = FontIndex::new(val);
let json = serde_json::to_string(&idx).unwrap();
let back: FontIndex = serde_json::from_str(&json).unwrap();
prop_assert_eq!(idx, back);
}
}
}