use crate::filter::FilterValue;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub trait CreateData: Send + Sync {
fn into_fields(self) -> HashMap<String, FieldValue>;
fn model_name() -> &'static str;
}
pub trait UpdateData: Send + Sync {
fn into_fields(self) -> HashMap<String, FieldValue>;
fn model_name() -> &'static str;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FieldValue {
Null,
Bool(bool),
Int(i64),
Float(f64),
String(String),
Json(serde_json::Value),
Bytes(Vec<u8>),
DateTime(String),
Uuid(String),
Array(Vec<FieldValue>),
Nested(Box<DataBuilder>),
Connect(ConnectData),
Disconnect,
Default,
Increment(i64),
Decrement(i64),
Multiply(f64),
Divide(f64),
Push(Box<FieldValue>),
Unset,
}
impl FieldValue {
pub fn to_filter_value(&self) -> Option<FilterValue> {
match self {
Self::Null => Some(FilterValue::Null),
Self::Bool(b) => Some(FilterValue::Bool(*b)),
Self::Int(i) => Some(FilterValue::Int(*i)),
Self::Float(f) => Some(FilterValue::Float(*f)),
Self::String(s) => Some(FilterValue::String(s.clone())),
Self::Json(j) => Some(FilterValue::Json(j.clone())),
Self::DateTime(s) => Some(FilterValue::String(s.clone())),
Self::Uuid(s) => Some(FilterValue::String(s.clone())),
_ => None,
}
}
}
impl From<bool> for FieldValue {
fn from(v: bool) -> Self {
Self::Bool(v)
}
}
impl From<i32> for FieldValue {
fn from(v: i32) -> Self {
Self::Int(v as i64)
}
}
impl From<i64> for FieldValue {
fn from(v: i64) -> Self {
Self::Int(v)
}
}
impl From<f32> for FieldValue {
fn from(v: f32) -> Self {
Self::Float(v as f64)
}
}
impl From<f64> for FieldValue {
fn from(v: f64) -> Self {
Self::Float(v)
}
}
impl From<String> for FieldValue {
fn from(v: String) -> Self {
Self::String(v)
}
}
impl From<&str> for FieldValue {
fn from(v: &str) -> Self {
Self::String(v.to_string())
}
}
impl From<serde_json::Value> for FieldValue {
fn from(v: serde_json::Value) -> Self {
Self::Json(v)
}
}
impl<T: Into<FieldValue>> From<Option<T>> for FieldValue {
fn from(v: Option<T>) -> Self {
match v {
Some(val) => val.into(),
None => Self::Null,
}
}
}
impl<T: Into<FieldValue>> From<Vec<T>> for FieldValue {
fn from(v: Vec<T>) -> Self {
Self::Array(v.into_iter().map(Into::into).collect())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectData {
pub field: String,
pub value: Box<FieldValue>,
}
impl ConnectData {
pub fn id(id: impl Into<FieldValue>) -> Self {
Self {
field: "id".to_string(),
value: Box::new(id.into()),
}
}
pub fn by(field: impl Into<String>, value: impl Into<FieldValue>) -> Self {
Self {
field: field.into(),
value: Box::new(value.into()),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DataBuilder {
fields: HashMap<String, FieldValue>,
}
impl DataBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn set(mut self, field: impl Into<String>, value: impl Into<FieldValue>) -> Self {
self.fields.insert(field.into(), value.into());
self
}
pub fn set_null(mut self, field: impl Into<String>) -> Self {
self.fields.insert(field.into(), FieldValue::Null);
self
}
pub fn set_default(mut self, field: impl Into<String>) -> Self {
self.fields.insert(field.into(), FieldValue::Default);
self
}
pub fn unset(mut self, field: impl Into<String>) -> Self {
self.fields.insert(field.into(), FieldValue::Unset);
self
}
pub fn increment(mut self, field: impl Into<String>, by: i64) -> Self {
self.fields.insert(field.into(), FieldValue::Increment(by));
self
}
pub fn decrement(mut self, field: impl Into<String>, by: i64) -> Self {
self.fields.insert(field.into(), FieldValue::Decrement(by));
self
}
pub fn multiply(mut self, field: impl Into<String>, by: f64) -> Self {
self.fields.insert(field.into(), FieldValue::Multiply(by));
self
}
pub fn divide(mut self, field: impl Into<String>, by: f64) -> Self {
self.fields.insert(field.into(), FieldValue::Divide(by));
self
}
pub fn push(mut self, field: impl Into<String>, value: impl Into<FieldValue>) -> Self {
self.fields
.insert(field.into(), FieldValue::Push(Box::new(value.into())));
self
}
pub fn connect(mut self, relation: impl Into<String>, id: impl Into<FieldValue>) -> Self {
self.fields
.insert(relation.into(), FieldValue::Connect(ConnectData::id(id)));
self
}
pub fn connect_by(
mut self,
relation: impl Into<String>,
field: impl Into<String>,
value: impl Into<FieldValue>,
) -> Self {
self.fields.insert(
relation.into(),
FieldValue::Connect(ConnectData::by(field, value)),
);
self
}
pub fn disconnect(mut self, relation: impl Into<String>) -> Self {
self.fields.insert(relation.into(), FieldValue::Disconnect);
self
}
pub fn create_nested(mut self, relation: impl Into<String>, data: DataBuilder) -> Self {
self.fields
.insert(relation.into(), FieldValue::Nested(Box::new(data)));
self
}
pub fn into_fields(self) -> HashMap<String, FieldValue> {
self.fields
}
pub fn has(&self, field: &str) -> bool {
self.fields.contains_key(field)
}
pub fn get(&self, field: &str) -> Option<&FieldValue> {
self.fields.get(field)
}
pub fn len(&self) -> usize {
self.fields.len()
}
pub fn is_empty(&self) -> bool {
self.fields.is_empty()
}
}
pub trait IntoData {
fn into_data(self) -> DataBuilder;
}
impl IntoData for DataBuilder {
fn into_data(self) -> DataBuilder {
self
}
}
impl IntoData for HashMap<String, FieldValue> {
fn into_data(self) -> DataBuilder {
DataBuilder { fields: self }
}
}
impl IntoData for serde_json::Value {
fn into_data(self) -> DataBuilder {
match self {
serde_json::Value::Object(map) => {
let fields = map
.into_iter()
.map(|(k, v)| (k, json_to_field_value(v)))
.collect();
DataBuilder { fields }
}
_ => DataBuilder::new(),
}
}
}
fn json_to_field_value(value: serde_json::Value) -> FieldValue {
match value {
serde_json::Value::Null => FieldValue::Null,
serde_json::Value::Bool(b) => FieldValue::Bool(b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
FieldValue::Int(i)
} else if let Some(f) = n.as_f64() {
FieldValue::Float(f)
} else {
FieldValue::Json(serde_json::Value::Number(n))
}
}
serde_json::Value::String(s) => FieldValue::String(s),
serde_json::Value::Array(arr) => {
FieldValue::Array(arr.into_iter().map(json_to_field_value).collect())
}
serde_json::Value::Object(_) => FieldValue::Json(value),
}
}
#[macro_export]
macro_rules! data {
() => {
$crate::data::DataBuilder::new()
};
($($field:ident : $value:expr),* $(,)?) => {{
let mut builder = $crate::data::DataBuilder::new();
$(
builder = builder.set(stringify!($field), $value);
)*
builder
}};
}
#[macro_export]
macro_rules! connect {
(id: $id:expr) => {
$crate::data::FieldValue::Connect($crate::data::ConnectData::id($id))
};
($field:ident : $value:expr) => {
$crate::data::FieldValue::Connect($crate::data::ConnectData::by(stringify!($field), $value))
};
}
#[macro_export]
macro_rules! increment {
($value:expr) => {
$crate::data::FieldValue::Increment($value)
};
}
#[macro_export]
macro_rules! decrement {
($value:expr) => {
$crate::data::FieldValue::Decrement($value)
};
}
#[derive(Debug, Clone, Default)]
pub struct BatchCreate<T> {
items: Vec<T>,
skip_duplicates: bool,
}
impl<T> BatchCreate<T> {
pub fn new(items: Vec<T>) -> Self {
Self {
items,
skip_duplicates: false,
}
}
pub fn skip_duplicates(mut self) -> Self {
self.skip_duplicates = true;
self
}
pub fn into_items(self) -> Vec<T> {
self.items
}
pub fn should_skip_duplicates(&self) -> bool {
self.skip_duplicates
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
}
impl<T> From<Vec<T>> for BatchCreate<T> {
fn from(items: Vec<T>) -> Self {
Self::new(items)
}
}
pub trait TypedCreateBuilder: Sized {
type Output: CreateData;
fn build(self) -> Self::Output;
}
pub trait TypedUpdateBuilder: Sized {
type Output: UpdateData;
fn build(self) -> Self::Output;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_data_builder_basic() {
let data = DataBuilder::new()
.set("name", "Bob")
.set("age", 30)
.set("active", true);
assert_eq!(data.len(), 3);
assert!(data.has("name"));
assert!(data.has("age"));
assert!(data.has("active"));
}
#[test]
fn test_data_builder_null_and_default() {
let data = DataBuilder::new()
.set_null("deleted_at")
.set_default("created_at");
assert!(matches!(data.get("deleted_at"), Some(FieldValue::Null)));
assert!(matches!(data.get("created_at"), Some(FieldValue::Default)));
}
#[test]
fn test_data_builder_numeric_operations() {
let data = DataBuilder::new()
.increment("views", 1)
.decrement("stock", 5)
.multiply("price", 1.1)
.divide("score", 2.0);
assert!(matches!(data.get("views"), Some(FieldValue::Increment(1))));
assert!(matches!(data.get("stock"), Some(FieldValue::Decrement(5))));
}
#[test]
fn test_data_builder_connect() {
let data = DataBuilder::new()
.connect("author", 1)
.connect_by("category", "slug", "tech");
assert!(matches!(data.get("author"), Some(FieldValue::Connect(_))));
assert!(matches!(data.get("category"), Some(FieldValue::Connect(_))));
}
#[test]
fn test_data_macro() {
let data = data! {
name: "Bob",
email: "bob@example.com",
age: 30,
};
assert_eq!(data.len(), 3);
assert!(matches!(data.get("name"), Some(FieldValue::String(s)) if s == "Bob"));
}
#[test]
#[allow(clippy::approx_constant)]
fn test_field_value_conversions() {
let _: FieldValue = true.into();
let _: FieldValue = 42_i32.into();
let _: FieldValue = 42_i64.into();
let _: FieldValue = 3.14_f64.into();
let _: FieldValue = "hello".into();
let _: FieldValue = String::from("hello").into();
let _: FieldValue = Some("optional").into();
let _: FieldValue = None::<String>.into();
let _: FieldValue = vec!["a", "b"].into();
}
#[test]
fn test_batch_create() {
let batch: BatchCreate<DataBuilder> =
vec![data! { name: "Alice" }, data! { name: "Bob" }].into();
assert_eq!(batch.len(), 2);
assert!(!batch.should_skip_duplicates());
let batch = batch.skip_duplicates();
assert!(batch.should_skip_duplicates());
}
#[test]
fn test_json_to_data() {
let json = serde_json::json!({
"name": "Bob",
"age": 30,
"active": true,
"tags": ["a", "b"]
});
let data: DataBuilder = json.into_data();
assert_eq!(data.len(), 4);
}
#[test]
fn test_connect_data() {
let by_id = ConnectData::id(1);
assert_eq!(by_id.field, "id");
let by_email = ConnectData::by("email", "bob@example.com");
assert_eq!(by_email.field, "email");
}
}