pathfinder_tagged/
lib.rs

1//! Tagging is meant to be used in tests only, but because `[cfg(test)]` cannot
2//! be _exported_ we're using the closest build configuration option within the
3//! implementation, which is `[cfg(debug_assertions)]`. As an additional safety
4//! measure, the [`tagged::init()`](crate::init()) function must be called
5//! before using the `tagged::Tagged` type.
6#![allow(dead_code)]
7use std::any::{Any, TypeId};
8use std::collections::HashMap;
9use std::fmt::{Debug, Formatter};
10use std::sync::{Arc, Mutex, MutexGuard, Once};
11
12use fake::{Dummy, Fake, Faker};
13
14/// As much as faking random(-ish) data for tests is pretty convenient,
15/// deciphering assertion failures is not so much.
16#[derive(Clone, Eq, PartialEq)]
17pub struct Tagged<T> {
18    pub tag: String,
19    pub data: T,
20}
21
22impl<T: Debug> Debug for Tagged<T> {
23    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
24        f.debug_struct("Tagged")
25            .field("tag", &self.tag)
26            .field("data", &self.data)
27            .finish()
28    }
29}
30
31type Lut = Option<Arc<Mutex<HashMap<TypeId, HashMap<String, Box<dyn Any>>>>>>;
32
33static INIT: Once = Once::new();
34static mut LUTS: Lut = None;
35
36type LutGuard = MutexGuard<'static, HashMap<TypeId, HashMap<String, Box<dyn Any>>>>;
37
38/// - Does nothing outside Debug builds.
39/// - This function __must__ be called before using the [`Tagged`] type, and if
40///   one then wants to see the tags in `std::fmt::Debug` output for each type
41///   that derives `tagged_debug_derive::TaggedDebug`.
42/// - You need to make sure to call this function __at least__ once. Subsequent
43///   calls will have no effect.
44pub fn init() {
45    #[cfg(debug_assertions)]
46    INIT.call_once(|| {
47        unsafe {
48            LUTS = Some(Default::default());
49        };
50    });
51}
52
53fn lut() -> Option<LutGuard> {
54    unsafe {
55        #[allow(static_mut_refs)]
56        LUTS.as_ref()
57    }
58    .map(|luts| luts.lock().unwrap())
59}
60
61impl<T: Clone + 'static> Tagged<T> {
62    /// Important
63    ///
64    /// Use only in Debug builds after calling [`tagged::init`](`crate::init`).
65    /// Otherwise this function always returns `None`.
66    pub fn get<U: ToString, C: FnOnce() -> T>(_tag: U, _ctor: C) -> Option<Self> {
67        #[cfg(debug_assertions)]
68        {
69            let luts = lut();
70            luts.map(|mut luts| {
71                let lut = luts.entry(TypeId::of::<T>()).or_default();
72                let tag = _tag.to_string();
73                let data = lut
74                    .entry(tag.clone())
75                    .or_insert_with(|| Box::new(_ctor()))
76                    .downcast_ref::<T>()
77                    .unwrap()
78                    .clone();
79                Self { tag, data }
80            })
81        }
82        #[cfg(not(debug_assertions))]
83        {
84            None
85        }
86    }
87}
88
89impl<T: Clone + Dummy<Faker> + 'static> Tagged<T> {
90    /// Important
91    ///
92    /// Use only in Debug builds after calling [`tagged::init`](`crate::init`).
93    /// Otherwise this function always returns `None`.
94    pub fn get_fake<U: ToString>(_tag: U) -> Option<Self> {
95        Self::get(_tag, || Faker.fake())
96    }
97}
98
99#[derive(Debug)]
100pub struct TypeNotFound;
101
102impl<T: Clone + PartialEq + 'static> Tagged<T> {
103    /// Important
104    ///
105    /// Use only in Debug builds after calling [`tagged::init`](`crate::init`).
106    /// Otherwise this function will error.
107    pub fn from_data(_data: &T) -> Result<Self, TypeNotFound> {
108        #[cfg(debug_assertions)]
109        {
110            let luts = lut().ok_or(TypeNotFound)?;
111            let lut = luts.get(&TypeId::of::<T>());
112
113            match lut {
114                Some(lut) => {
115                    let tag = lut
116                        .iter()
117                        .find_map(|(k, v)| {
118                            v.downcast_ref::<T>()
119                                .and_then(|u| (u == _data).then_some(k.clone()))
120                        })
121                        .unwrap_or("value not found".into());
122
123                    Ok(Self {
124                        tag,
125                        data: _data.clone(),
126                    })
127                }
128                None => Err(TypeNotFound),
129            }
130        }
131        #[cfg(not(debug_assertions))]
132        Err(TypeNotFound)
133    }
134
135    /// Important
136    ///
137    /// Use only in Debug builds after calling [`tagged::init`](`crate::init`).
138    /// Otherwise this function will error.
139    pub fn tag(data: &T) -> Result<String, TypeNotFound> {
140        Self::from_data(data).map(|tagged| tagged.tag)
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use fake::Dummy;
147    use pathfinder_tagged_debug_derive::TaggedDebug;
148    use pretty_assertions_sorted::assert_eq;
149
150    use super::*;
151
152    #[derive(Clone, Copy, Default, Dummy, PartialEq, TaggedDebug)]
153    struct Unit;
154
155    #[derive(Clone, Copy, Default, Dummy, PartialEq, TaggedDebug)]
156    struct Tuple(i32, i32);
157
158    #[derive(Clone, Copy, Default, Dummy, PartialEq, TaggedDebug)]
159    struct Struct {
160        a: i32,
161        b: i32,
162    }
163
164    #[derive(Clone, Copy, Default, Dummy, PartialEq, TaggedDebug)]
165    enum Enum {
166        #[default]
167        A,
168        B(i32, i32),
169        C {
170            a: i32,
171            b: i32,
172        },
173    }
174
175    #[derive(Clone, Copy, Default, Dummy, PartialEq, TaggedDebug)]
176    struct Complex {
177        u: Unit,
178        t: Tuple,
179        e: Enum,
180    }
181
182    #[test]
183    fn lookup_and_debugs_work_correctly() {
184        let unit = Unit;
185        let tuple = Tuple(0, 1);
186        let stru = Struct { a: 0, b: 1 };
187        let enum_unit = Enum::A;
188        let enum_tuple = Enum::B(0, 1);
189        let enum_struct = Enum::C { a: 2, b: 3 };
190        let complex = Complex {
191            u: Unit,
192            t: Tuple(0, 1),
193            e: Enum::C { a: 2, b: 3 },
194        };
195
196        // Global lut is not initialized yet, so show default debugs
197        assert_eq!(format!("{:?}", unit), "Unit");
198        assert_eq!(format!("{:?}", tuple), "Tuple(0, 1)");
199        assert_eq!(format!("{:?}", stru), "Struct { a: 0, b: 1 }");
200        assert_eq!(format!("{:?}", enum_unit), "A");
201        assert_eq!(format!("{:?}", enum_tuple), "B(0, 1)");
202        assert_eq!(format!("{:?}", enum_struct), r#"C { a: 2, b: 3 }"#);
203        assert_eq!(
204            format!("{:?}", complex),
205            r#"Complex { u: Unit, t: Tuple(0, 1), e: C { a: 2, b: 3 } }"#
206        );
207
208        // Global lut needs to be initialized to cache values and retrieve them
209        assert!(Tagged::<Unit>::get("unit", || unit).is_none());
210        assert!(Tagged::<Complex>::get_fake("complex").is_none());
211        assert!(Tagged::from_data(&complex).is_err());
212        assert!(Tagged::tag(&complex).is_err());
213
214        // Init global lut
215        crate::init();
216
217        // These types are not registered yet, so still show default debugs
218        assert_eq!(format!("{:?}", unit), "Unit");
219        assert_eq!(format!("{:?}", tuple), "Tuple(0, 1)");
220        assert_eq!(format!("{:?}", stru), "Struct { a: 0, b: 1 }");
221        assert_eq!(format!("{:?}", enum_unit), "A");
222        assert_eq!(format!("{:?}", enum_tuple), "B(0, 1)");
223        assert_eq!(format!("{:?}", enum_struct), r#"C { a: 2, b: 3 }"#);
224        assert_eq!(
225            format!("{:?}", complex),
226            r#"Complex { u: Unit, t: Tuple(0, 1), e: C { a: 2, b: 3 } }"#
227        );
228
229        // Register types by inserting at least one value per type, now the debugs for
230        // those values should show the tag they were created with
231        Tagged::<Unit>::get_fake("unit");
232        Tagged::<Tuple>::get("tuple", || tuple);
233        Tagged::<Struct>::get("struct", || stru);
234        Tagged::<Enum>::get("enum_unit", || enum_unit);
235        Tagged::<Enum>::get("enum_tuple", || enum_tuple);
236        Tagged::<Enum>::get("enum_struct", || enum_struct);
237        Tagged::<Complex>::get("complex", || complex);
238
239        assert_eq!(format!("{:?}", unit), r#"Unit { TAG: "unit" }"#);
240        assert_eq!(format!("{:?}", tuple), r#"Tuple("TAG: tuple", 0, 1)"#);
241        assert_eq!(
242            format!("{:?}", stru),
243            r#"Struct { TAG: "struct", a: 0, b: 1 }"#
244        );
245        assert_eq!(format!("{:?}", enum_unit), r#"A { TAG: "enum_unit" }"#);
246        assert_eq!(format!("{:?}", enum_tuple), r#"B("TAG: enum_tuple", 0, 1)"#);
247        assert_eq!(
248            format!("{:?}", enum_struct),
249            r#"C { TAG: "enum_struct", a: 2, b: 3 }"#
250        );
251        assert_eq!(
252            format!("{:?}", complex),
253            r#"Complex { TAG: "complex", u: Unit { TAG: "unit" }, t: Tuple("TAG: tuple", 0, 1), e: C { TAG: "enum_struct", a: 2, b: 3 } }"#
254        );
255    }
256}