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