fluidattacks-database-core-domain 0.1.0

Database core functional core: database-agnostic table identifiers and types (no I/O)
Documentation
/// Declares a table once: the struct and its [`Table`](crate::table::Table) impl.
///
/// The schema and the values come from the same list of fields. Column names
/// are the field names (`r#type` is the column `type`), and key fields must be
/// [`KeyField`](crate::cell::KeyField)s.
///
/// ```
/// use fluidattacks_database_core_domain::cell::Numeric;
/// use fluidattacks_database_core_domain::table;
///
/// table! {
///     #[derive(Debug, Clone)]
///     pub struct Expense {
///         pub id: i64,
///         pub description: Option<String>,
///         pub amount: Numeric<14, 2>,
///     }
///     key = [id];
/// }
/// ```
#[macro_export]
macro_rules! table {
    (
        $(#[$meta:meta])*
        $vis:vis struct $name:ident {
            $( $(#[$fmeta:meta])* $fvis:vis $field:ident : $ty:ty ),+ $(,)?
        }
        key = [ $( $key:ident ),* $(,)? ];
    ) => {
        $(#[$meta])*
        $vis struct $name {
            $( $(#[$fmeta])* $fvis $field : $ty ),+
        }

        impl $crate::table::Table for $name {
            fn schema() -> ::core::result::Result<$crate::table::Schema, $crate::table::SchemaError> {
                $( let _ = |row: &$name| $crate::__private::is_key(&row.$key); )*
                $crate::table::Schema::new(
                    [ $(
                        $crate::table::Column::new(
                            $crate::core::ColumnId::new($crate::core::Identifier::new(
                                ::core::stringify!($field).trim_start_matches("r#"),
                            )),
                            <$ty as $crate::cell::FieldType>::cell_type(),
                            <$ty as $crate::cell::FieldType>::nullable(),
                        )
                    ),+ ]
                    .into_iter()
                    .collect(),
                    [ $(
                        $crate::core::ColumnId::new($crate::core::Identifier::new(
                            ::core::stringify!($key).trim_start_matches("r#"),
                        ))
                    ),* ]
                    .into_iter()
                    .collect(),
                )
            }

            fn values(self) -> $crate::__private::Vec<$crate::cell::RawValue> {
                [ $( $crate::cell::IntoValue::into_value(self.$field) ),+ ]
                    .into_iter()
                    .collect()
            }
        }
    };
}

/// Key fields are checked at compile time. Each case below is the same table,
/// changing only its key; this one compiles:
///
/// ```
/// # use fluidattacks_database_core_domain::table;
/// table! {
///     struct Row { id: i64, note: Option<String>, tags: Vec<String>, payload: serde_json::Value, score: f64 }
///     key = [id];
/// }
/// ```
///
/// A key that is not a field does not compile:
///
/// ```compile_fail
/// # use fluidattacks_database_core_domain::table;
/// table! {
///     struct Row { id: i64, note: Option<String>, tags: Vec<String>, payload: serde_json::Value, score: f64 }
///     key = [missing];
/// }
/// ```
///
/// Nor does a nullable key:
///
/// ```compile_fail
/// # use fluidattacks_database_core_domain::table;
/// table! {
///     struct Row { id: i64, note: Option<String>, tags: Vec<String>, payload: serde_json::Value, score: f64 }
///     key = [note];
/// }
/// ```
///
/// Nor a list:
///
/// ```compile_fail
/// # use fluidattacks_database_core_domain::table;
/// table! {
///     struct Row { id: i64, note: Option<String>, tags: Vec<String>, payload: serde_json::Value, score: f64 }
///     key = [tags];
/// }
/// ```
///
/// Nor JSON:
///
/// ```compile_fail
/// # use fluidattacks_database_core_domain::table;
/// table! {
///     struct Row { id: i64, note: Option<String>, tags: Vec<String>, payload: serde_json::Value, score: f64 }
///     key = [payload];
/// }
/// ```
///
/// Nor a float:
///
/// ```compile_fail
/// # use fluidattacks_database_core_domain::table;
/// table! {
///     struct Row { id: i64, note: Option<String>, tags: Vec<String>, payload: serde_json::Value, score: f64 }
///     key = [score];
/// }
/// ```
mod key_checks {}

#[cfg(test)]
mod tests {
    use alloc::boxed::Box;
    use alloc::string::String;
    use alloc::vec;
    use alloc::vec::Vec;

    use crate::cell::{CellType, Decimal, Numeric, RawValue};
    use crate::core::{ColumnId, Identifier};
    use crate::table::{Column, Schema, Table};

    crate::table! {
        struct Expense {
            id: i64,
            description: Option<String>,
            amount: Numeric<14, 2>,
            tags: Vec<String>,
        }
        key = [id];
    }

    crate::table! {
        struct Kind {
            r#type: i64,
        }
        key = [r#type];
    }

    crate::table! {
        struct Event {
            payload: serde_json::Value,
        }
        key = [];
    }

    fn id(name: &str) -> ColumnId {
        ColumnId::new(Identifier::new(name))
    }

    #[test]
    fn the_schema_comes_from_the_fields() {
        let expected = Schema::new(
            vec![
                Column::new(id("id"), CellType::Integer, false),
                Column::new(id("description"), CellType::Text, true),
                Column::new(
                    id("amount"),
                    CellType::Decimal {
                        precision: 14,
                        scale: 2,
                    },
                    false,
                ),
                Column::new(
                    id("tags"),
                    CellType::Array {
                        element: Box::new(CellType::Text),
                        nullable_elements: false,
                    },
                    false,
                ),
            ],
            vec![id("id")],
        );
        assert_eq!(Expense::schema(), expected);
    }

    #[test]
    fn the_values_follow_the_schema_order() {
        let values = Numeric::new(12_345).map(|amount| {
            Expense {
                id: 7,
                description: None,
                amount,
                tags: vec![String::from("travel")],
            }
            .values()
        });
        let expected = Decimal::new(12_345, 2).map(|amount| {
            vec![
                RawValue::Integer(7),
                RawValue::Null,
                RawValue::Decimal(amount),
                RawValue::Array(vec![RawValue::Text(String::from("travel"))]),
            ]
        });
        assert_eq!(values, expected);
    }

    #[test]
    fn raw_identifiers_name_their_column_without_the_prefix() {
        assert_eq!(
            Kind::schema().map(|s| s.key().to_vec()),
            Ok(vec![id("type")])
        );
    }

    #[test]
    fn a_table_may_be_declared_with_no_key() {
        assert_eq!(Event::schema().map(|s| s.key().is_empty()), Ok(true));
    }
}