realm_db_reader/
model.rs

1#[doc(hidden)]
2#[macro_export]
3macro_rules! realm_model_field {
4    ($struct:ident, $row:ident, $field:ident = $alias:expr) => {
5        $row.take($alias)
6            .ok_or_else(|| $crate::ValueError::MissingField {
7                field: $alias,
8                target_type: stringify!($struct),
9                remaining_fields: $row.clone().into_owned(),
10            })?
11            .try_into()?
12    };
13    ($struct:ident, $row:ident, $field:ident) => {
14        $crate::realm_model_field!($struct, $row, $field = stringify!($field))
15    };
16}
17
18/// Macro to implement conversion from a Row to a Realm model struct. This allows for easy creation
19/// of your own struct instances, based on data retrieved from a Realm database.
20///
21/// ```rust
22/// use realm_db_reader::realm_model;
23///
24/// struct MyStruct {
25///     field1: String,
26///     field2: i64,
27/// }
28///
29/// realm_model!(MyStruct => field1, field2);
30/// ```
31///
32/// You may only use types that either are valid Realm values, or can themselves
33/// be converted from Realm values. The builtin types are:
34///
35/// - `String` and `Option<String>`
36/// - `i64` and `Option<i64>`
37/// - `bool` and `Option<bool>`
38/// - `f32`
39/// - `f64`
40/// - `chrono::DateTime<Utc>` and `Option<chrono::DateTime<Utc>>`
41/// - [`Link`](crate::Link), `Option<Link>`, and `Vec<Link>`
42///
43/// All struct fields must be present, but you may omit columns that you don't
44/// need. The types of the fields in your struct should, of course, match the
45/// types of the Realm table columns.
46///
47/// # Renaming fields
48///
49/// If you want to name a field differently, you can use the `=` syntax to
50/// specify an alias:
51///
52/// ```rust
53/// use realm_db_reader::realm_model;
54///
55/// struct MyStruct {
56///     my_struct_field: String,
57///     my_other_struct_field: i64,
58/// }
59///
60/// realm_model!(MyStruct => my_struct_field, my_other_struct_field = "realmColumnName");
61/// ```
62///
63/// # Backlinks
64///
65/// Some tables in Realm can be linked to each other using backlinks. To define
66/// a backlink, you can use the `;` syntax to specify the name of your backlink
67/// field:
68///
69/// ```rust
70/// use realm_db_reader::{realm_model, Backlink};
71///
72/// struct MyStruct {
73///     field1: String,
74///     field2: i64,
75///     backlink_field: Vec<Backlink>,
76/// }
77///
78/// realm_model!(MyStruct => field1, field2; backlink_field);
79/// ```
80///
81/// This will create a backlink field in the struct that can be used to retrieve
82/// all rows that link to the current row. Backlink fields are unnamed in Realm,
83/// which is why they don't follow the same conventions as other fields.
84///
85/// # Subtables
86///
87/// In the case where the Realm table contains a subtable, you can refer to this
88/// data too:
89///
90/// ```rust
91/// use realm_db_reader::realm_model;
92///
93/// struct MyStruct {
94///     id: String,
95///     // A subtable that contains a list of strings.
96///     strings: Vec<String>,
97///     // A subtable that contains complete data.
98///     items: Vec<Item>,
99/// }
100///
101/// realm_model!(MyStruct => id, strings, items);
102///
103/// struct Item {
104///     subtable_row_id: String,
105///     subtable_row_content: String,
106/// }
107///
108/// // The aliases are not required here, it's just to illustrate they're
109/// // available in subtables too.
110/// realm_model!(Item => subtable_row_id = "id", subtable_row_content = "content");
111/// ```
112#[macro_export]
113macro_rules! realm_model {
114    ($struct:ident => $($field:ident$(= $alias:expr)?),*$(; $backlinks:ident)?) => {
115        impl<'a> ::core::convert::TryFrom<$crate::Row<'a>> for $struct {
116            type Error = $crate::ValueError;
117
118            fn try_from(mut row: $crate::Row<'a>) -> $crate::ValueResult<Self> {
119                $(
120                let $field = $crate::realm_model_field!($struct, row, $field$(= $alias)?);
121                )*
122                $(
123                let $backlinks = row.take_backlinks();
124                )?
125
126                Ok(Self {
127                    $(
128                        $field,
129                    )*
130                    $(
131                        $backlinks,
132                    )?
133                })
134            }
135        }
136    };
137}
138
139#[cfg(test)]
140mod tests {
141    use crate::value::ARRAY_VALUE_KEY;
142    use crate::{Backlink, Link, Row, Value};
143    use itertools::*;
144
145    #[test]
146    fn test_realm_model() {
147        struct MyModel {
148            id: String,
149            foo: Option<String>,
150            bar: Option<chrono::DateTime<chrono::Utc>>,
151            baz: i64,
152            qux: Option<i64>,
153            other: bool,
154            items: Vec<String>,
155            sub_items: Vec<SubModel>,
156        }
157
158        #[derive(Debug, PartialEq)]
159        struct SubModel {
160            left: i64,
161            right: i64,
162        }
163
164        realm_model!(MyModel => id, foo, bar, baz, qux, other = "!invalid_rust_alias", items, sub_items = "children");
165        realm_model!(SubModel => left, right);
166
167        let foo_values = [Some("hello".to_string()), None];
168        let bar_values = [Some(chrono::Utc::now()), None];
169        let qux_values = [Some(42), None];
170
171        for (foo_value, bar_value, qux_value) in iproduct!(foo_values, bar_values, qux_values) {
172            let values: Vec<Value> = vec![
173                "id_value".into(),
174                foo_value.clone().into(),
175                bar_value.into(),
176                "extra_field".into(),
177                100.into(),
178                qux_value.into(),
179                true.into(),
180                "extra_field".into(),
181                vec![
182                    Row::new(vec!["member1".into()], vec![ARRAY_VALUE_KEY.into()]),
183                    Row::new(vec!["member2".into()], vec![ARRAY_VALUE_KEY.into()]),
184                ]
185                .into(),
186                vec![
187                    Row::new(
188                        vec![1.into(), 2.into()],
189                        vec!["left".into(), "right".into()],
190                    ),
191                    Row::new(
192                        vec![3.into(), 4.into()],
193                        vec!["left".into(), "right".into()],
194                    ),
195                ]
196                .into(),
197            ];
198            let row = Row::new(
199                values,
200                vec![
201                    "id".into(),
202                    "foo".into(),
203                    "bar".into(),
204                    "some_other_field".into(),
205                    "baz".into(),
206                    "qux".into(),
207                    "!invalid_rust_alias".into(),
208                    "another_field".into(),
209                    "items".into(),
210                    "children".into(),
211                ],
212            );
213
214            let my_model: MyModel = row.try_into().unwrap();
215            assert_eq!(my_model.id, "id_value");
216            assert_eq!(my_model.foo, foo_value);
217            assert_eq!(my_model.bar, bar_value);
218            assert_eq!(my_model.baz, 100);
219            assert_eq!(my_model.qux, qux_value);
220            assert!(my_model.other);
221            assert_eq!(
222                my_model.items,
223                vec!["member1".to_string(), "member2".to_string()]
224            );
225            assert_eq!(
226                my_model.sub_items,
227                vec![
228                    SubModel { left: 1, right: 2 },
229                    SubModel { left: 3, right: 4 }
230                ]
231            );
232        }
233    }
234
235    #[test]
236    fn test_model_with_links() {
237        struct MyModel {
238            id: String,
239            link_a: Link,
240            // FIXME: This is not supported yet
241            // link_b: Vec<Link>,
242            optional_link: Option<Link>,
243            backlinks: Vec<Backlink>,
244        }
245
246        realm_model!(MyModel => id, link_a, optional_link; backlinks);
247
248        let values = vec![
249            "123456789".into(),
250            "irrelevant_field".into(),
251            Link::new(12, 5).into(),
252            vec![Link::new(13, 6)].into(),
253            Value::None,
254            Backlink::new(12, 5, vec![1989]).into(),
255        ];
256        let row = Row::new(
257            values,
258            vec![
259                "id".into(),
260                "other_field".into(),
261                "link_a".into(),
262                "link_b".into(),
263                "optional_link".into(),
264                // NOTE: backlinks are unnamed
265            ],
266        );
267
268        let model: MyModel = row.try_into().unwrap();
269        assert_eq!(model.id, "123456789");
270        assert_eq!(model.backlinks, vec![Backlink::new(12, 5, vec![1989])]);
271        assert_eq!(model.link_a, Link::new(12, 5));
272        assert_eq!(model.optional_link, None);
273    }
274}