Skip to main content

fluidattacks_database_core_domain/table/
macros.rs

1/// Declares a table once: the struct and its [`Table`](crate::table::Table) impl.
2///
3/// The schema and the values come from the same list of fields. Column names
4/// are the field names (`r#type` is the column `type`), and key fields must be
5/// [`KeyField`](crate::cell::KeyField)s.
6///
7/// ```
8/// use fluidattacks_database_core_domain::cell::Numeric;
9/// use fluidattacks_database_core_domain::table;
10///
11/// table! {
12///     #[derive(Debug, Clone)]
13///     pub struct Expense {
14///         pub id: i64,
15///         pub description: Option<String>,
16///         pub amount: Numeric<14, 2>,
17///     }
18///     key = [id];
19/// }
20/// ```
21#[macro_export]
22macro_rules! table {
23    (
24        $(#[$meta:meta])*
25        $vis:vis struct $name:ident {
26            $( $(#[$fmeta:meta])* $fvis:vis $field:ident : $ty:ty ),+ $(,)?
27        }
28        key = [ $( $key:ident ),* $(,)? ];
29    ) => {
30        $(#[$meta])*
31        $vis struct $name {
32            $( $(#[$fmeta])* $fvis $field : $ty ),+
33        }
34
35        impl $crate::table::Table for $name {
36            fn schema() -> ::core::result::Result<$crate::table::Schema, $crate::table::SchemaError> {
37                $( let _ = |row: &$name| $crate::__private::is_key(&row.$key); )*
38                $crate::table::Schema::new(
39                    [ $(
40                        $crate::table::Column::new(
41                            $crate::core::ColumnId::new($crate::core::Identifier::new(
42                                ::core::stringify!($field).trim_start_matches("r#"),
43                            )),
44                            <$ty as $crate::cell::FieldType>::cell_type(),
45                            <$ty as $crate::cell::FieldType>::nullable(),
46                        )
47                    ),+ ]
48                    .into_iter()
49                    .collect(),
50                    [ $(
51                        $crate::core::ColumnId::new($crate::core::Identifier::new(
52                            ::core::stringify!($key).trim_start_matches("r#"),
53                        ))
54                    ),* ]
55                    .into_iter()
56                    .collect(),
57                )
58            }
59
60            fn values(self) -> $crate::__private::Vec<$crate::cell::RawValue> {
61                [ $( $crate::cell::IntoValue::into_value(self.$field) ),+ ]
62                    .into_iter()
63                    .collect()
64            }
65        }
66    };
67}
68
69/// Key fields are checked at compile time. Each case below is the same table,
70/// changing only its key; this one compiles:
71///
72/// ```
73/// # use fluidattacks_database_core_domain::table;
74/// table! {
75///     struct Row { id: i64, note: Option<String>, tags: Vec<String>, payload: serde_json::Value, score: f64 }
76///     key = [id];
77/// }
78/// ```
79///
80/// A key that is not a field does not compile:
81///
82/// ```compile_fail
83/// # use fluidattacks_database_core_domain::table;
84/// table! {
85///     struct Row { id: i64, note: Option<String>, tags: Vec<String>, payload: serde_json::Value, score: f64 }
86///     key = [missing];
87/// }
88/// ```
89///
90/// Nor does a nullable key:
91///
92/// ```compile_fail
93/// # use fluidattacks_database_core_domain::table;
94/// table! {
95///     struct Row { id: i64, note: Option<String>, tags: Vec<String>, payload: serde_json::Value, score: f64 }
96///     key = [note];
97/// }
98/// ```
99///
100/// Nor a list:
101///
102/// ```compile_fail
103/// # use fluidattacks_database_core_domain::table;
104/// table! {
105///     struct Row { id: i64, note: Option<String>, tags: Vec<String>, payload: serde_json::Value, score: f64 }
106///     key = [tags];
107/// }
108/// ```
109///
110/// Nor JSON:
111///
112/// ```compile_fail
113/// # use fluidattacks_database_core_domain::table;
114/// table! {
115///     struct Row { id: i64, note: Option<String>, tags: Vec<String>, payload: serde_json::Value, score: f64 }
116///     key = [payload];
117/// }
118/// ```
119///
120/// Nor a float:
121///
122/// ```compile_fail
123/// # use fluidattacks_database_core_domain::table;
124/// table! {
125///     struct Row { id: i64, note: Option<String>, tags: Vec<String>, payload: serde_json::Value, score: f64 }
126///     key = [score];
127/// }
128/// ```
129mod key_checks {}
130
131#[cfg(test)]
132mod tests {
133    use alloc::boxed::Box;
134    use alloc::string::String;
135    use alloc::vec;
136    use alloc::vec::Vec;
137
138    use crate::cell::{CellType, Decimal, Numeric, RawValue};
139    use crate::core::{ColumnId, Identifier};
140    use crate::table::{Column, Schema, Table};
141
142    crate::table! {
143        struct Expense {
144            id: i64,
145            description: Option<String>,
146            amount: Numeric<14, 2>,
147            tags: Vec<String>,
148        }
149        key = [id];
150    }
151
152    crate::table! {
153        struct Kind {
154            r#type: i64,
155        }
156        key = [r#type];
157    }
158
159    crate::table! {
160        struct Event {
161            payload: serde_json::Value,
162        }
163        key = [];
164    }
165
166    fn id(name: &str) -> ColumnId {
167        ColumnId::new(Identifier::new(name))
168    }
169
170    #[test]
171    fn the_schema_comes_from_the_fields() {
172        let expected = Schema::new(
173            vec![
174                Column::new(id("id"), CellType::Integer, false),
175                Column::new(id("description"), CellType::Text, true),
176                Column::new(
177                    id("amount"),
178                    CellType::Decimal {
179                        precision: 14,
180                        scale: 2,
181                    },
182                    false,
183                ),
184                Column::new(
185                    id("tags"),
186                    CellType::Array {
187                        element: Box::new(CellType::Text),
188                        nullable_elements: false,
189                    },
190                    false,
191                ),
192            ],
193            vec![id("id")],
194        );
195        assert_eq!(Expense::schema(), expected);
196    }
197
198    #[test]
199    fn the_values_follow_the_schema_order() {
200        let values = Numeric::new(12_345).map(|amount| {
201            Expense {
202                id: 7,
203                description: None,
204                amount,
205                tags: vec![String::from("travel")],
206            }
207            .values()
208        });
209        let expected = Decimal::new(12_345, 2).map(|amount| {
210            vec![
211                RawValue::Integer(7),
212                RawValue::Null,
213                RawValue::Decimal(amount),
214                RawValue::Array(vec![RawValue::Text(String::from("travel"))]),
215            ]
216        });
217        assert_eq!(values, expected);
218    }
219
220    #[test]
221    fn raw_identifiers_name_their_column_without_the_prefix() {
222        assert_eq!(
223            Kind::schema().map(|s| s.key().to_vec()),
224            Ok(vec![id("type")])
225        );
226    }
227
228    #[test]
229    fn a_table_may_be_declared_with_no_key() {
230        assert_eq!(Event::schema().map(|s| s.key().is_empty()), Ok(true));
231    }
232}