use crate::BindValues;
#[cfg(feature = "merge-patch")]
use queryweave::FieldValues;
use queryweave::{Conditions, PersistedFields, SortKeys};
use sqlx::QueryBuilder;
#[cfg(feature = "merge-patch")]
use sqlx::query_builder::Separated;
pub trait Columns {
const NAME: &'static str;
const COLUMNS: &'static [(&'static str, &'static str)];
#[must_use]
fn column(field: &str) -> Option<&'static str> {
column_for(Self::COLUMNS, field)
}
fn push_conditions<DB: BindValues, M: PersistedFields>(
query_builder: &mut QueryBuilder<'_, DB>,
conditions: Conditions<M>,
separator: &str,
) {
const { assert_model_columns::<Self, M>() };
crate::sql::push_conditions::<Self, DB, M>(query_builder, conditions, separator);
}
fn push_order_by<DB: BindValues, M: PersistedFields>(
query_builder: &mut QueryBuilder<'_, DB>,
sort_keys: SortKeys<M>,
) {
const { assert_model_columns::<Self, M>() };
crate::sql::push_order_by::<Self, DB, M>(query_builder, sort_keys);
}
#[cfg(feature = "merge-patch")]
fn push_assignments<'qb, 'args, DB: BindValues, M: PersistedFields>(
query_builder: &'qb mut QueryBuilder<'args, DB>,
assignments: FieldValues<M>,
) -> Separated<'qb, 'args, DB, &'static str> {
const { assert_model_columns::<Self, M>() };
crate::sql::push_assignments::<Self, DB, M>(query_builder, assignments)
}
}
const fn str_eq(left: &str, right: &str) -> bool {
let (left, right) = (left.as_bytes(), right.as_bytes());
if left.len() != right.len() {
return false;
}
let mut index = 0;
while index < left.len() {
if left[index] != right[index] {
return false;
}
index += 1;
}
true
}
#[must_use]
pub const fn column_for(
columns: &'static [(&'static str, &'static str)],
field: &str,
) -> Option<&'static str> {
let mut index = 0;
while index < columns.len() {
let (domain_field, column) = columns[index];
if str_eq(domain_field, field) {
return Some(column);
}
index += 1;
}
None
}
#[must_use]
pub const fn first_field_without_column(
columns: &'static [(&'static str, &'static str)],
fields: &'static [&'static str],
) -> Option<&'static str> {
let mut index = 0;
while index < fields.len() {
if column_for(columns, fields[index]).is_none() {
return Some(fields[index]);
}
index += 1;
}
None
}
const MESSAGE_CAPACITY: usize = 512;
pub struct Message {
bytes: [u8; MESSAGE_CAPACITY],
len: usize,
}
impl Message {
const fn push(mut self, text: &str) -> Self {
let text = text.as_bytes();
let mut index = 0;
while index < text.len() && self.len < MESSAGE_CAPACITY {
self.bytes[self.len] = text[index];
self.len += 1;
index += 1;
}
self
}
#[must_use]
pub const fn as_str(&self) -> &str {
match core::str::from_utf8(self.bytes.split_at(self.len).0) {
Ok(text) => text,
Err(_) => "a domain field has no column",
}
}
}
#[must_use]
pub const fn missing_column_message(entity: &str, field: &str) -> Message {
Message {
bytes: [0; MESSAGE_CAPACITY],
len: 0,
}
.push("domain field `")
.push(field)
.push("` has no column on `")
.push(entity)
.push("`: add a field of that name to the entity, or map one onto it with `#[domain_field(")
.push(field)
.push(")]`")
}
#[macro_export]
macro_rules! assert_columns {
($entity:ty, $fields:expr $(,)?) => {
const _: () = {
if let ::core::option::Option::Some(field) =
$crate::first_field_without_column(<$entity as $crate::Columns>::COLUMNS, $fields)
{
::core::panic!(
"{}",
$crate::missing_column_message(<$entity as $crate::Columns>::NAME, field)
.as_str()
);
}
};
};
}
#[allow(
clippy::panic,
reason = "a const-evaluation panic is how a compile-time check fails the build"
)]
#[mutants::skip]
pub const fn assert_model_columns<C: Columns + ?Sized, M: PersistedFields>() {
if let Some(field) = first_field_without_column(C::COLUMNS, M::FIELD_NAMES) {
panic!("{}", missing_column_message(C::NAME, field).as_str());
}
}
#[cfg(test)]
mod tests {
use super::{Columns, column_for, first_field_without_column, missing_column_message, str_eq};
use pretty_assertions::assert_eq;
struct Order;
impl Columns for Order {
const NAME: &'static str = "Order";
const COLUMNS: &'static [(&'static str, &'static str)] =
&[("number", "number"), ("delivery_date", "delivered_at")];
}
#[test]
fn str_eq_compares_whole_strings() {
assert!(str_eq("number", "number"));
assert!(!str_eq("number", "numbers"));
assert!(!str_eq("number", "Number"));
assert!(str_eq("", ""));
}
#[test]
fn a_field_maps_onto_its_column() {
assert_eq!(Order::column("number"), Some("number"));
assert_eq!(Order::column("delivery_date"), Some("delivered_at"));
assert_eq!(Order::column("client"), None);
assert_eq!(column_for(Order::COLUMNS, "delivered_at"), None);
}
#[test]
fn the_first_field_without_a_column_is_reported() {
assert_eq!(
first_field_without_column(Order::COLUMNS, &["number", "delivery_date"]),
None
);
assert_eq!(
first_field_without_column(Order::COLUMNS, &["number", "client", "value"]),
Some("client")
);
}
crate::assert_columns!(Order, &["delivery_date", "number"]);
#[test]
fn the_message_names_the_field_and_the_entity() {
assert_eq!(
missing_column_message("Order", "client").as_str(),
"domain field `client` has no column on `Order`: add a field of that name to the entity, or map one onto it with `#[domain_field(client)]`"
);
}
#[test]
fn an_overlong_message_is_cut_at_the_capacity() {
let field = "f".repeat(600);
assert_eq!(
missing_column_message("Order", &field).as_str().len(),
super::MESSAGE_CAPACITY
);
}
}