ixa/people/
property.rs

1//! Properties are the main way to store and access data about people.
2//!
3//! # Properties
4//!
5//! Properties are defined using the `define_person_property!` and
6//! `define_derived_property!` macros.
7//!
8//! # Multi-properties
9//!
10//! The `define_multi_property!` macro (defined in `property.rs`) takes a name and a tuple
11//! of property tags. It defines a derived property (via `define_derived_property`) with the
12//! provided name having the type of tuples of values corresponding to the provided tags.
13//!
14//! ```rust,ignore
15//! use ixa::people::{define_multi_property, define_person_property};
16//!
17//! define_person_property!(Name, &'static str);
18//! define_person_property!(Age, u8);
19//! define_person_property!(Weight, f64);
20//!
21//! define_multi_property!(Profile, (Name, Age, Weight));
22//! ```
23//!
24//! The new derived property is not automatically indexed. You can index it just
25//! like any other property:
26//!
27//! ```rust, ignore
28//! context.index_property(Profile);
29//! ```
30
31use std::any::TypeId;
32use std::fmt::Debug;
33
34use serde::Serialize;
35
36use crate::hashing::hash_serialized_128;
37use crate::people::data::PersonPropertyHolder;
38use crate::{Context, PersonId};
39
40/// We factor this out and provide a blanket implementation for all types that
41/// can be value types for properties. This makes it convenient to reference
42/// [`PersonPropertyValue`] trait constraints.
43pub trait PersonPropertyValue: Copy + Debug + PartialEq + Serialize {}
44impl<T> PersonPropertyValue for T where T: Copy + Debug + PartialEq + Serialize {}
45
46/// An individual characteristic or state related to a person, such as age or
47/// disease status.
48///
49/// Person properties should be defined with the [`define_person_property!`](crate::define_person_property!),
50/// [`define_person_property_with_default!`](crate::define_person_property_with_default!) and
51/// [`define_derived_property!`](crate::define_derived_property!) macros.
52pub trait PersonProperty: Copy + 'static {
53    /// The type of the property's values.
54    type Value: PersonPropertyValue;
55    /// Some properties might store a transformed version of the value in the index. This is the
56    /// type of the transformed value. For simple properties this will be the same as [`Self::Value`].
57    type CanonicalValue: PersonPropertyValue;
58
59    #[must_use]
60    fn is_derived() -> bool {
61        false
62    }
63
64    #[must_use]
65    fn is_required() -> bool {
66        false
67    }
68
69    #[must_use]
70    fn dependencies() -> Vec<Box<dyn PersonPropertyHolder>> {
71        panic!("Dependencies not implemented");
72    }
73
74    fn register_dependencies(_: &Context) {
75        panic!("Dependencies not implemented");
76    }
77
78    fn compute(context: &Context, person_id: PersonId) -> Self::Value;
79
80    /// This transforms a [`Self::Value`] into a [`Self::CanonicalValue`], e.g. for storage in an index.
81    /// For simple properties, this is the identity function.
82    #[must_use]
83    fn make_canonical(value: Self::Value) -> Self::CanonicalValue;
84
85    /// The inverse transform of [`make_canonical`](Self::make_canonical). For simple properties, this is the identity function.
86    #[must_use]
87    fn make_uncanonical(value: Self::CanonicalValue) -> Self::Value;
88    fn get_instance() -> Self;
89    fn name() -> &'static str;
90
91    /// Returns a string representation of the property value, e.g. for writing to a CSV file.
92    /// If [`make_uncanonical`](Self::make_uncanonical) is nontrivial, this method usually transforms `value` into a
93    /// [`Self::Value`] first so that the value is formatted in a way the user expects.
94    #[must_use]
95    fn get_display(value: &Self::CanonicalValue) -> String;
96
97    /// For cases when the property's hash needs to be computed in a special way.
98    #[must_use]
99    fn hash_property_value(value: &Self::CanonicalValue) -> u128 {
100        hash_serialized_128(value)
101    }
102
103    /// Overridden by multi-properties, which use the [`TypeId`](std::any::TypeId) of the ordered tuple so that tuples
104    /// with the same component types in a different order will have the same type ID.
105    #[must_use]
106    fn type_id() -> TypeId {
107        TypeId::of::<Self>()
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::people::{PeoplePlugin, Query};
115    use crate::prelude::*;
116    use crate::PersonProperty;
117
118    define_person_property!(Pu32, u32);
119    define_person_property!(POu32, Option<u32>);
120
121    define_person_property!(Name, &'static str);
122    define_person_property!(Age, u8);
123    define_person_property!(Weight, f64);
124
125    define_multi_property!(ProfileNAW, (Name, Age, Weight));
126    define_multi_property!(ProfileAWN, (Age, Weight, Name));
127    define_multi_property!(ProfileWAN, (Weight, Age, Name));
128
129    #[test]
130    fn test_multi_property_ordering() {
131        let a: <ProfileNAW as PersonProperty>::Value = ("Jane", 22, 180.5);
132        let b: <ProfileAWN as PersonProperty>::Value = (22, 180.5, "Jane");
133        let c: <ProfileWAN as PersonProperty>::Value = (180.5, 22, "Jane");
134
135        assert_eq!(ProfileNAW::type_id(), ProfileAWN::type_id());
136        assert_eq!(ProfileNAW::type_id(), ProfileWAN::type_id());
137
138        let a_canonical: <ProfileNAW as PersonProperty>::CanonicalValue =
139            ProfileNAW::make_canonical(a);
140        let b_canonical: <ProfileAWN as PersonProperty>::CanonicalValue =
141            ProfileAWN::make_canonical(b);
142        let c_canonical: <ProfileWAN as PersonProperty>::CanonicalValue =
143            ProfileWAN::make_canonical(c);
144
145        assert_eq!(a_canonical, b_canonical);
146        assert_eq!(a_canonical, c_canonical);
147
148        // Actually, all of the `Profile***::hash_property_value` methods should be the same,
149        // so we could use any single one.
150        assert_eq!(
151            ProfileNAW::hash_property_value(&a_canonical),
152            ProfileAWN::hash_property_value(&b_canonical)
153        );
154        assert_eq!(
155            ProfileNAW::hash_property_value(&a_canonical),
156            ProfileWAN::hash_property_value(&c_canonical)
157        );
158
159        // Since the canonical values are the same, we could have used any single one, but this
160        // demonstrates that we can convert from one order to another.
161        assert_eq!(ProfileNAW::make_uncanonical(b_canonical), a);
162        assert_eq!(ProfileAWN::make_uncanonical(c_canonical), b);
163        assert_eq!(ProfileWAN::make_uncanonical(a_canonical), c);
164    }
165
166    #[test]
167    fn test_multi_property_vs_property_query() {
168        let mut context = Context::new();
169
170        context
171            .add_person(((Name, "John"), (Age, 42), (Weight, 220.5)))
172            .unwrap();
173        context
174            .add_person(((Name, "Jane"), (Age, 22), (Weight, 180.5)))
175            .unwrap();
176        context
177            .add_person(((Name, "Bob"), (Age, 32), (Weight, 190.5)))
178            .unwrap();
179        context
180            .add_person(((Name, "Alice"), (Age, 22), (Weight, 170.5)))
181            .unwrap();
182
183        context.index_property(ProfileNAW);
184
185        {
186            let data = context.get_data(PeoplePlugin);
187            assert!(data
188                .property_indexes
189                .borrow()
190                .get(&ProfileNAW::type_id())
191                .is_some());
192        }
193
194        {
195            let example_query = ((Name, "Alice"), (Age, 22), (Weight, 170.5));
196            let query_multi_property_type_id = Query::multi_property_type_id(&example_query);
197            assert!(query_multi_property_type_id.is_some());
198            assert_eq!(ProfileNAW::type_id(), query_multi_property_type_id.unwrap());
199            assert_eq!(
200                Query::multi_property_value_hash(&example_query),
201                ProfileNAW::hash_property_value(&ProfileNAW::make_canonical(("Alice", 22, 170.5)))
202            );
203        }
204
205        context.with_query_results((ProfileNAW, ("John", 42, 220.5)), &mut |results| {
206            assert_eq!(results.len(), 1);
207        });
208    }
209
210    #[test]
211    fn test_get_display() {
212        let mut context = Context::new();
213        let person = context.add_person(((POu32, Some(42)), (Pu32, 22))).unwrap();
214        assert_eq!(
215            format!(
216                "{:}",
217                POu32::get_display(&context.get_person_property(person, POu32))
218            ),
219            "42"
220        );
221        assert_eq!(
222            format!(
223                "{:}",
224                Pu32::get_display(&context.get_person_property(person, Pu32))
225            ),
226            "22"
227        );
228        let person2 = context.add_person(((POu32, None), (Pu32, 11))).unwrap();
229        assert_eq!(
230            format!(
231                "{:}",
232                POu32::get_display(&context.get_person_property(person2, POu32))
233            ),
234            "None"
235        );
236    }
237
238    #[test]
239    fn test_debug_trait() {
240        let property = Pu32;
241        let debug_str = format!("{:?}", property);
242        assert_eq!(debug_str, "Pu32");
243
244        let property = POu32;
245        let debug_str = format!("{:?}", property);
246        assert_eq!(debug_str, "POu32");
247    }
248}