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}