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}