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}