prax_query/
data.rs

1//! Ergonomic data creation utilities.
2//!
3//! This module provides multiple ways to create data for insert/update operations,
4//! from simple struct-based approaches to flexible builders.
5//!
6//! # Approaches
7//!
8//! ## 1. Struct-Based (Recommended for simple creates)
9//!
10//! ```rust,ignore
11//! // Generated struct with required/optional fields
12//! let user = client.user().create(UserCreate {
13//!     email: "bob@example.com".into(),
14//!     name: Some("Bob".into()),
15//!     ..Default::default()
16//! }).exec().await?;
17//! ```
18//!
19//! ## 2. Builder Pattern (For complex creates with relations)
20//!
21//! ```rust,ignore
22//! let user = client.user()
23//!     .create_with(|b| b
24//!         .email("bob@example.com")
25//!         .name("Bob")
26//!         .posts(vec![
27//!             PostCreate { title: "Hello".into(), ..Default::default() }
28//!         ])
29//!     )
30//!     .exec().await?;
31//! ```
32//!
33//! ## 3. Macro (Ultra-concise)
34//!
35//! ```rust,ignore
36//! let user = client.user().create(data! {
37//!     email: "bob@example.com",
38//!     name: "Bob",
39//! }).exec().await?;
40//! ```
41//!
42//! ## 4. From tuples (Quick and dirty)
43//!
44//! ```rust,ignore
45//! // For models with few required fields
46//! let user = client.user().create(("bob@example.com", "Bob")).exec().await?;
47//! ```
48
49use crate::filter::FilterValue;
50use serde::{Deserialize, Serialize};
51use std::collections::HashMap;
52
53/// Trait for types that can be used as create data.
54pub trait CreateData: Send + Sync {
55    /// Get the field values as a map.
56    fn into_fields(self) -> HashMap<String, FieldValue>;
57
58    /// Get the model name.
59    fn model_name() -> &'static str;
60}
61
62/// Trait for types that can be used as update data.
63pub trait UpdateData: Send + Sync {
64    /// Get the field values as a map (only set fields).
65    fn into_fields(self) -> HashMap<String, FieldValue>;
66
67    /// Get the model name.
68    fn model_name() -> &'static str;
69}
70
71/// A field value that can be set in create/update operations.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(untagged)]
74pub enum FieldValue {
75    /// Null value.
76    Null,
77    /// Boolean value.
78    Bool(bool),
79    /// Integer value.
80    Int(i64),
81    /// Float value.
82    Float(f64),
83    /// String value.
84    String(String),
85    /// JSON value.
86    Json(serde_json::Value),
87    /// Bytes value.
88    Bytes(Vec<u8>),
89    /// DateTime as ISO string.
90    DateTime(String),
91    /// UUID as string.
92    Uuid(String),
93    /// Array of values.
94    Array(Vec<FieldValue>),
95    /// Nested create data.
96    Nested(Box<DataBuilder>),
97    /// Connect to existing record.
98    Connect(ConnectData),
99    /// Disconnect from related record.
100    Disconnect,
101    /// Set to default value.
102    Default,
103    /// Increment by value.
104    Increment(i64),
105    /// Decrement by value.
106    Decrement(i64),
107    /// Multiply by value.
108    Multiply(f64),
109    /// Divide by value.
110    Divide(f64),
111    /// Append to array.
112    Push(Box<FieldValue>),
113    /// Unset the field.
114    Unset,
115}
116
117impl FieldValue {
118    /// Convert to FilterValue for query operations.
119    pub fn to_filter_value(&self) -> Option<FilterValue> {
120        match self {
121            Self::Null => Some(FilterValue::Null),
122            Self::Bool(b) => Some(FilterValue::Bool(*b)),
123            Self::Int(i) => Some(FilterValue::Int(*i)),
124            Self::Float(f) => Some(FilterValue::Float(*f)),
125            Self::String(s) => Some(FilterValue::String(s.clone())),
126            Self::Json(j) => Some(FilterValue::Json(j.clone())),
127            Self::DateTime(s) => Some(FilterValue::String(s.clone())),
128            Self::Uuid(s) => Some(FilterValue::String(s.clone())),
129            _ => None,
130        }
131    }
132}
133
134// Convenient From implementations
135impl From<bool> for FieldValue {
136    fn from(v: bool) -> Self {
137        Self::Bool(v)
138    }
139}
140
141impl From<i32> for FieldValue {
142    fn from(v: i32) -> Self {
143        Self::Int(v as i64)
144    }
145}
146
147impl From<i64> for FieldValue {
148    fn from(v: i64) -> Self {
149        Self::Int(v)
150    }
151}
152
153impl From<f32> for FieldValue {
154    fn from(v: f32) -> Self {
155        Self::Float(v as f64)
156    }
157}
158
159impl From<f64> for FieldValue {
160    fn from(v: f64) -> Self {
161        Self::Float(v)
162    }
163}
164
165impl From<String> for FieldValue {
166    fn from(v: String) -> Self {
167        Self::String(v)
168    }
169}
170
171impl From<&str> for FieldValue {
172    fn from(v: &str) -> Self {
173        Self::String(v.to_string())
174    }
175}
176
177impl From<serde_json::Value> for FieldValue {
178    fn from(v: serde_json::Value) -> Self {
179        Self::Json(v)
180    }
181}
182
183impl<T: Into<FieldValue>> From<Option<T>> for FieldValue {
184    fn from(v: Option<T>) -> Self {
185        match v {
186            Some(val) => val.into(),
187            None => Self::Null,
188        }
189    }
190}
191
192impl<T: Into<FieldValue>> From<Vec<T>> for FieldValue {
193    fn from(v: Vec<T>) -> Self {
194        Self::Array(v.into_iter().map(Into::into).collect())
195    }
196}
197
198/// Data for connecting to an existing record.
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct ConnectData {
201    /// Field name to match on.
202    pub field: String,
203    /// Value to match.
204    pub value: Box<FieldValue>,
205}
206
207impl ConnectData {
208    /// Create a connect by ID.
209    pub fn id(id: impl Into<FieldValue>) -> Self {
210        Self {
211            field: "id".to_string(),
212            value: Box::new(id.into()),
213        }
214    }
215
216    /// Create a connect by a specific field.
217    pub fn by(field: impl Into<String>, value: impl Into<FieldValue>) -> Self {
218        Self {
219            field: field.into(),
220            value: Box::new(value.into()),
221        }
222    }
223}
224
225/// A flexible data builder for create/update operations.
226///
227/// This builder allows setting fields dynamically and supports
228/// nested creates, connects, and all update operations.
229#[derive(Debug, Clone, Default, Serialize, Deserialize)]
230pub struct DataBuilder {
231    fields: HashMap<String, FieldValue>,
232}
233
234impl DataBuilder {
235    /// Create a new empty data builder.
236    pub fn new() -> Self {
237        Self::default()
238    }
239
240    /// Set a field value.
241    pub fn set(mut self, field: impl Into<String>, value: impl Into<FieldValue>) -> Self {
242        self.fields.insert(field.into(), value.into());
243        self
244    }
245
246    /// Set a field to null.
247    pub fn set_null(mut self, field: impl Into<String>) -> Self {
248        self.fields.insert(field.into(), FieldValue::Null);
249        self
250    }
251
252    /// Set a field to its default value.
253    pub fn set_default(mut self, field: impl Into<String>) -> Self {
254        self.fields.insert(field.into(), FieldValue::Default);
255        self
256    }
257
258    /// Unset a field (for updates).
259    pub fn unset(mut self, field: impl Into<String>) -> Self {
260        self.fields.insert(field.into(), FieldValue::Unset);
261        self
262    }
263
264    /// Increment a numeric field.
265    pub fn increment(mut self, field: impl Into<String>, by: i64) -> Self {
266        self.fields.insert(field.into(), FieldValue::Increment(by));
267        self
268    }
269
270    /// Decrement a numeric field.
271    pub fn decrement(mut self, field: impl Into<String>, by: i64) -> Self {
272        self.fields.insert(field.into(), FieldValue::Decrement(by));
273        self
274    }
275
276    /// Multiply a numeric field.
277    pub fn multiply(mut self, field: impl Into<String>, by: f64) -> Self {
278        self.fields.insert(field.into(), FieldValue::Multiply(by));
279        self
280    }
281
282    /// Divide a numeric field.
283    pub fn divide(mut self, field: impl Into<String>, by: f64) -> Self {
284        self.fields.insert(field.into(), FieldValue::Divide(by));
285        self
286    }
287
288    /// Push a value to an array field.
289    pub fn push(mut self, field: impl Into<String>, value: impl Into<FieldValue>) -> Self {
290        self.fields
291            .insert(field.into(), FieldValue::Push(Box::new(value.into())));
292        self
293    }
294
295    /// Connect to an existing related record by ID.
296    pub fn connect(mut self, relation: impl Into<String>, id: impl Into<FieldValue>) -> Self {
297        self.fields
298            .insert(relation.into(), FieldValue::Connect(ConnectData::id(id)));
299        self
300    }
301
302    /// Connect to an existing related record by a specific field.
303    pub fn connect_by(
304        mut self,
305        relation: impl Into<String>,
306        field: impl Into<String>,
307        value: impl Into<FieldValue>,
308    ) -> Self {
309        self.fields.insert(
310            relation.into(),
311            FieldValue::Connect(ConnectData::by(field, value)),
312        );
313        self
314    }
315
316    /// Disconnect from a related record.
317    pub fn disconnect(mut self, relation: impl Into<String>) -> Self {
318        self.fields.insert(relation.into(), FieldValue::Disconnect);
319        self
320    }
321
322    /// Create a nested record.
323    pub fn create_nested(mut self, relation: impl Into<String>, data: DataBuilder) -> Self {
324        self.fields
325            .insert(relation.into(), FieldValue::Nested(Box::new(data)));
326        self
327    }
328
329    /// Get the fields map.
330    pub fn into_fields(self) -> HashMap<String, FieldValue> {
331        self.fields
332    }
333
334    /// Check if a field is set.
335    pub fn has(&self, field: &str) -> bool {
336        self.fields.contains_key(field)
337    }
338
339    /// Get a field value.
340    pub fn get(&self, field: &str) -> Option<&FieldValue> {
341        self.fields.get(field)
342    }
343
344    /// Get the number of fields set.
345    pub fn len(&self) -> usize {
346        self.fields.len()
347    }
348
349    /// Check if no fields are set.
350    pub fn is_empty(&self) -> bool {
351        self.fields.is_empty()
352    }
353}
354
355/// Helper trait for converting to DataBuilder.
356pub trait IntoData {
357    /// Convert into a DataBuilder.
358    fn into_data(self) -> DataBuilder;
359}
360
361impl IntoData for DataBuilder {
362    fn into_data(self) -> DataBuilder {
363        self
364    }
365}
366
367impl IntoData for HashMap<String, FieldValue> {
368    fn into_data(self) -> DataBuilder {
369        DataBuilder { fields: self }
370    }
371}
372
373impl IntoData for serde_json::Value {
374    fn into_data(self) -> DataBuilder {
375        match self {
376            serde_json::Value::Object(map) => {
377                let fields = map
378                    .into_iter()
379                    .map(|(k, v)| (k, json_to_field_value(v)))
380                    .collect();
381                DataBuilder { fields }
382            }
383            _ => DataBuilder::new(),
384        }
385    }
386}
387
388fn json_to_field_value(value: serde_json::Value) -> FieldValue {
389    match value {
390        serde_json::Value::Null => FieldValue::Null,
391        serde_json::Value::Bool(b) => FieldValue::Bool(b),
392        serde_json::Value::Number(n) => {
393            if let Some(i) = n.as_i64() {
394                FieldValue::Int(i)
395            } else if let Some(f) = n.as_f64() {
396                FieldValue::Float(f)
397            } else {
398                FieldValue::Json(serde_json::Value::Number(n))
399            }
400        }
401        serde_json::Value::String(s) => FieldValue::String(s),
402        serde_json::Value::Array(arr) => {
403            FieldValue::Array(arr.into_iter().map(json_to_field_value).collect())
404        }
405        serde_json::Value::Object(_) => FieldValue::Json(value),
406    }
407}
408
409/// Macro for concise data creation.
410///
411/// # Examples
412///
413/// ```rust,ignore
414/// use prax_query::data;
415///
416/// // Simple create
417/// let user_data = data! {
418///     email: "bob@example.com",
419///     name: "Bob",
420///     age: 30,
421/// };
422///
423/// // With nested data
424/// let post_data = data! {
425///     title: "Hello World",
426///     author: connect!(id: 1),
427///     tags: ["rust", "orm"],
428/// };
429///
430/// // With optional fields
431/// let update_data = data! {
432///     name: "Robert",
433///     bio: null,
434///     views: increment!(1),
435/// };
436/// ```
437#[macro_export]
438macro_rules! data {
439    // Empty data
440    () => {
441        $crate::data::DataBuilder::new()
442    };
443
444    // Data with fields
445    ($($field:ident : $value:expr),* $(,)?) => {{
446        let mut builder = $crate::data::DataBuilder::new();
447        $(
448            builder = builder.set(stringify!($field), $value);
449        )*
450        builder
451    }};
452}
453
454/// Macro for creating connection data.
455#[macro_export]
456macro_rules! connect {
457    (id: $id:expr) => {
458        $crate::data::FieldValue::Connect($crate::data::ConnectData::id($id))
459    };
460    ($field:ident : $value:expr) => {
461        $crate::data::FieldValue::Connect($crate::data::ConnectData::by(stringify!($field), $value))
462    };
463}
464
465/// Macro for increment operations.
466#[macro_export]
467macro_rules! increment {
468    ($value:expr) => {
469        $crate::data::FieldValue::Increment($value)
470    };
471}
472
473/// Macro for decrement operations.
474#[macro_export]
475macro_rules! decrement {
476    ($value:expr) => {
477        $crate::data::FieldValue::Decrement($value)
478    };
479}
480
481/// Batch create helper for creating multiple records.
482#[derive(Debug, Clone, Default)]
483pub struct BatchCreate<T> {
484    items: Vec<T>,
485    skip_duplicates: bool,
486}
487
488impl<T> BatchCreate<T> {
489    /// Create a new batch create.
490    pub fn new(items: Vec<T>) -> Self {
491        Self {
492            items,
493            skip_duplicates: false,
494        }
495    }
496
497    /// Skip duplicate records instead of failing.
498    pub fn skip_duplicates(mut self) -> Self {
499        self.skip_duplicates = true;
500        self
501    }
502
503    /// Get the items.
504    pub fn into_items(self) -> Vec<T> {
505        self.items
506    }
507
508    /// Check if duplicates should be skipped.
509    pub fn should_skip_duplicates(&self) -> bool {
510        self.skip_duplicates
511    }
512
513    /// Get the number of items.
514    pub fn len(&self) -> usize {
515        self.items.len()
516    }
517
518    /// Check if empty.
519    pub fn is_empty(&self) -> bool {
520        self.items.is_empty()
521    }
522}
523
524impl<T> From<Vec<T>> for BatchCreate<T> {
525    fn from(items: Vec<T>) -> Self {
526        Self::new(items)
527    }
528}
529
530/// Builder for creating records with a fluent API.
531///
532/// This is generated per-model and provides type-safe field setters.
533///
534/// # Example (Generated Code)
535///
536/// ```rust,ignore
537/// // This is what gets generated for a User model:
538/// pub struct UserCreateBuilder {
539///     data: DataBuilder,
540/// }
541///
542/// impl UserCreateBuilder {
543///     pub fn email(self, email: impl Into<String>) -> Self { ... }
544///     pub fn name(self, name: impl Into<String>) -> Self { ... }
545///     pub fn age(self, age: i32) -> Self { ... }
546///     pub fn posts(self, posts: Vec<PostCreate>) -> Self { ... }
547/// }
548/// ```
549pub trait TypedCreateBuilder: Sized {
550    /// The output type.
551    type Output: CreateData;
552
553    /// Build the create data.
554    fn build(self) -> Self::Output;
555}
556
557/// Builder for updating records with a fluent API.
558pub trait TypedUpdateBuilder: Sized {
559    /// The output type.
560    type Output: UpdateData;
561
562    /// Build the update data.
563    fn build(self) -> Self::Output;
564}
565
566#[cfg(test)]
567mod tests {
568    use super::*;
569
570    #[test]
571    fn test_data_builder_basic() {
572        let data = DataBuilder::new()
573            .set("name", "Bob")
574            .set("age", 30)
575            .set("active", true);
576
577        assert_eq!(data.len(), 3);
578        assert!(data.has("name"));
579        assert!(data.has("age"));
580        assert!(data.has("active"));
581    }
582
583    #[test]
584    fn test_data_builder_null_and_default() {
585        let data = DataBuilder::new()
586            .set_null("deleted_at")
587            .set_default("created_at");
588
589        assert!(matches!(data.get("deleted_at"), Some(FieldValue::Null)));
590        assert!(matches!(data.get("created_at"), Some(FieldValue::Default)));
591    }
592
593    #[test]
594    fn test_data_builder_numeric_operations() {
595        let data = DataBuilder::new()
596            .increment("views", 1)
597            .decrement("stock", 5)
598            .multiply("price", 1.1)
599            .divide("score", 2.0);
600
601        assert!(matches!(data.get("views"), Some(FieldValue::Increment(1))));
602        assert!(matches!(data.get("stock"), Some(FieldValue::Decrement(5))));
603    }
604
605    #[test]
606    fn test_data_builder_connect() {
607        let data = DataBuilder::new()
608            .connect("author", 1)
609            .connect_by("category", "slug", "tech");
610
611        assert!(matches!(data.get("author"), Some(FieldValue::Connect(_))));
612        assert!(matches!(data.get("category"), Some(FieldValue::Connect(_))));
613    }
614
615    #[test]
616    fn test_data_macro() {
617        let data = data! {
618            name: "Bob",
619            email: "bob@example.com",
620            age: 30,
621        };
622
623        assert_eq!(data.len(), 3);
624        assert!(matches!(data.get("name"), Some(FieldValue::String(s)) if s == "Bob"));
625    }
626
627    #[test]
628    #[allow(clippy::approx_constant)]
629    fn test_field_value_conversions() {
630        let _: FieldValue = true.into();
631        let _: FieldValue = 42_i32.into();
632        let _: FieldValue = 42_i64.into();
633        let _: FieldValue = 3.14_f64.into();
634        let _: FieldValue = "hello".into();
635        let _: FieldValue = String::from("hello").into();
636        let _: FieldValue = Some("optional").into();
637        let _: FieldValue = None::<String>.into();
638        let _: FieldValue = vec!["a", "b"].into();
639    }
640
641    #[test]
642    fn test_batch_create() {
643        let batch: BatchCreate<DataBuilder> =
644            vec![data! { name: "Alice" }, data! { name: "Bob" }].into();
645
646        assert_eq!(batch.len(), 2);
647        assert!(!batch.should_skip_duplicates());
648
649        let batch = batch.skip_duplicates();
650        assert!(batch.should_skip_duplicates());
651    }
652
653    #[test]
654    fn test_json_to_data() {
655        let json = serde_json::json!({
656            "name": "Bob",
657            "age": 30,
658            "active": true,
659            "tags": ["a", "b"]
660        });
661
662        let data: DataBuilder = json.into_data();
663        assert_eq!(data.len(), 4);
664    }
665
666    #[test]
667    fn test_connect_data() {
668        let by_id = ConnectData::id(1);
669        assert_eq!(by_id.field, "id");
670
671        let by_email = ConnectData::by("email", "bob@example.com");
672        assert_eq!(by_email.field, "email");
673    }
674}