toasty_driver_integration_suite/tests/
embed_enum_data.rs

1use crate::prelude::*;
2
3/// Verifies that a data-carrying enum has its variant fields registered in the app
4/// schema with globally-assigned field indices (indices are unique across all variants).
5#[driver_test]
6pub async fn data_carrying_enum_schema(test: &mut Test) {
7    #[allow(dead_code)]
8    #[derive(toasty::Embed)]
9    enum ContactInfo {
10        #[column(variant = 1)]
11        Email { address: String },
12        #[column(variant = 2)]
13        Phone { number: String },
14    }
15
16    let db = test.setup_db(models!(ContactInfo)).await;
17    let schema = db.schema();
18
19    assert_struct!(schema.app.models, #{
20        ContactInfo::id(): toasty::schema::app::Model::EmbeddedEnum({
21            name.upper_camel_case(): "ContactInfo",
22            variants: [
23                {
24                    name.upper_camel_case(): "Email",
25                    discriminant: toasty_core::stmt::Value::I64(1),
26                    ..
27                },
28                {
29                    name.upper_camel_case(): "Phone",
30                    discriminant: toasty_core::stmt::Value::I64(2),
31                    ..
32                },
33            ],
34            fields: [
35                { id.index: 0, name.app: Some("address") },
36                { id.index: 1, name.app: Some("number") },
37            ],
38        }),
39    });
40}
41
42/// Verifies that a mixed enum (some unit variants, some data variants) registers
43/// correctly: unit variants have empty `fields`, data variants have their fields
44/// with indices assigned starting from 0 and continuing globally across variants.
45#[driver_test]
46pub async fn mixed_enum_schema(test: &mut Test) {
47    #[allow(dead_code)]
48    #[derive(toasty::Embed)]
49    enum Status {
50        #[column(variant = 1)]
51        Pending,
52        #[column(variant = 2)]
53        Failed { reason: String },
54        #[column(variant = 3)]
55        Done,
56    }
57
58    let db = test.setup_db(models!(Status)).await;
59    let schema = db.schema();
60
61    assert_struct!(schema.app.models, #{
62        Status::id(): toasty::schema::app::Model::EmbeddedEnum({
63            variants: [
64                {
65                    name.upper_camel_case(): "Pending",
66                    discriminant: toasty_core::stmt::Value::I64(1),
67                    ..
68                },
69                {
70                    name.upper_camel_case(): "Failed",
71                    discriminant: toasty_core::stmt::Value::I64(2),
72                    ..
73                },
74                {
75                    name.upper_camel_case(): "Done",
76                    discriminant: toasty_core::stmt::Value::I64(3),
77                    ..
78                },
79            ],
80            fields: [
81                { id.index: 0, name.app: Some("reason") },
82            ],
83        }),
84    });
85}
86
87/// Verifies DB columns for a data-carrying enum: discriminant column + one nullable
88/// column per variant field, named `{disc_col}_{field_name}`.
89#[driver_test]
90pub async fn data_carrying_enum_db_schema(test: &mut Test) {
91    #[allow(dead_code)]
92    #[derive(toasty::Embed)]
93    enum ContactInfo {
94        #[column(variant = 1)]
95        Email { address: String },
96        #[column(variant = 2)]
97        Phone { number: String },
98    }
99
100    #[derive(toasty::Model)]
101    struct User {
102        #[key]
103        id: String,
104        #[allow(dead_code)]
105        contact: ContactInfo,
106    }
107
108    let db = test.setup_db(models!(User, ContactInfo)).await;
109    let schema = db.schema();
110
111    // The DB table has disc col + one col per variant field (2 variants × 1 field each).
112    assert_struct!(schema.db.tables, [
113        {
114            name: =~ r"users$",
115            columns: [
116                { name: "id" },
117                { name: "contact", nullable: false },
118                { name: "contact_address", nullable: true },
119                { name: "contact_number", nullable: true },
120            ],
121        },
122    ]);
123}
124
125/// End-to-end CRUD test for a data-carrying enum (all variants have fields).
126/// Creates records with different variants, reads them back, and verifies roundtrip.
127#[driver_test]
128pub async fn data_variant_roundtrip(test: &mut Test) -> Result<()> {
129    #[derive(Debug, PartialEq, toasty::Embed)]
130    enum ContactInfo {
131        #[column(variant = 1)]
132        Email { address: String },
133        #[column(variant = 2)]
134        Phone { number: String },
135    }
136
137    #[derive(Debug, toasty::Model)]
138    struct User {
139        #[key]
140        #[auto]
141        id: uuid::Uuid,
142        name: String,
143        contact: ContactInfo,
144    }
145
146    let mut db = test.setup_db(models!(User, ContactInfo)).await;
147
148    let alice = User::create()
149        .name("Alice")
150        .contact(ContactInfo::Email {
151            address: "alice@example.com".to_string(),
152        })
153        .exec(&mut db)
154        .await?;
155
156    let bob = User::create()
157        .name("Bob")
158        .contact(ContactInfo::Phone {
159            number: "555-1234".to_string(),
160        })
161        .exec(&mut db)
162        .await?;
163
164    // Read back and check values are reconstructed correctly.
165    let found_alice = User::get_by_id(&mut db, &alice.id).await?;
166    assert_eq!(
167        found_alice.contact,
168        ContactInfo::Email {
169            address: "alice@example.com".to_string()
170        }
171    );
172
173    let found_bob = User::get_by_id(&mut db, &bob.id).await?;
174    assert_eq!(
175        found_bob.contact,
176        ContactInfo::Phone {
177            number: "555-1234".to_string()
178        }
179    );
180
181    // Clean up.
182    alice.delete().exec(&mut db).await?;
183    bob.delete().exec(&mut db).await?;
184    Ok(())
185}
186
187/// End-to-end CRUD test for a mixed enum (unit variants and data variants).
188/// Verifies that both kinds round-trip correctly through the DB.
189#[driver_test]
190pub async fn mixed_enum_roundtrip(test: &mut Test) -> Result<()> {
191    #[derive(Debug, PartialEq, toasty::Embed)]
192    enum Status {
193        #[column(variant = 1)]
194        Pending,
195        #[column(variant = 2)]
196        Failed { reason: String },
197        #[column(variant = 3)]
198        Done,
199    }
200
201    #[derive(Debug, toasty::Model)]
202    struct Task {
203        #[key]
204        #[auto]
205        id: uuid::Uuid,
206        title: String,
207        status: Status,
208    }
209
210    let mut db = test.setup_db(models!(Task, Status)).await;
211
212    let pending = Task::create()
213        .title("Pending task")
214        .status(Status::Pending)
215        .exec(&mut db)
216        .await?;
217
218    let failed = Task::create()
219        .title("Failed task")
220        .status(Status::Failed {
221            reason: "out of memory".to_string(),
222        })
223        .exec(&mut db)
224        .await?;
225
226    let done = Task::create()
227        .title("Done task")
228        .status(Status::Done)
229        .exec(&mut db)
230        .await?;
231
232    let found_pending = Task::get_by_id(&mut db, &pending.id).await?;
233    assert_eq!(found_pending.status, Status::Pending);
234
235    let found_failed = Task::get_by_id(&mut db, &failed.id).await?;
236    assert_eq!(
237        found_failed.status,
238        Status::Failed {
239            reason: "out of memory".to_string()
240        }
241    );
242
243    let found_done = Task::get_by_id(&mut db, &done.id).await?;
244    assert_eq!(found_done.status, Status::Done);
245
246    Ok(())
247}
248
249/// Tests that UUID fields inside data-carrying enum variants round-trip correctly.
250/// UUID is a non-trivial primitive that requires type casting on some databases.
251#[driver_test]
252pub async fn data_variant_with_uuid_field(test: &mut Test) -> Result<()> {
253    #[derive(Debug, PartialEq, toasty::Embed)]
254    enum OrderRef {
255        #[column(variant = 1)]
256        Internal { id: uuid::Uuid },
257        #[column(variant = 2)]
258        External { code: String },
259    }
260
261    #[derive(Debug, toasty::Model)]
262    struct Order {
263        #[key]
264        #[auto]
265        id: uuid::Uuid,
266        order_ref: OrderRef,
267    }
268
269    let mut db = test.setup_db(models!(Order, OrderRef)).await;
270
271    let internal_id = uuid::Uuid::new_v4();
272
273    let o1 = Order::create()
274        .order_ref(OrderRef::Internal { id: internal_id })
275        .exec(&mut db)
276        .await?;
277
278    let o2 = Order::create()
279        .order_ref(OrderRef::External {
280            code: "EXT-001".to_string(),
281        })
282        .exec(&mut db)
283        .await?;
284
285    let found_o1 = Order::get_by_id(&mut db, &o1.id).await?;
286    assert_eq!(found_o1.order_ref, OrderRef::Internal { id: internal_id });
287
288    let found_o2 = Order::get_by_id(&mut db, &o2.id).await?;
289    assert_eq!(
290        found_o2.order_ref,
291        OrderRef::External {
292            code: "EXT-001".to_string()
293        }
294    );
295
296    Ok(())
297}
298
299/// Tests that jiff::Timestamp fields inside data-carrying enum variants round-trip correctly.
300/// Also covers a mixed enum (one unit variant, one data variant) to verify null handling.
301#[driver_test]
302pub async fn data_variant_with_jiff_timestamp(test: &mut Test) -> Result<()> {
303    #[derive(Debug, PartialEq, toasty::Embed)]
304    enum EventTime {
305        #[column(variant = 1)]
306        Scheduled { at: jiff::Timestamp },
307        #[column(variant = 2)]
308        Unscheduled,
309    }
310
311    #[derive(Debug, toasty::Model)]
312    struct Event {
313        #[key]
314        #[auto]
315        id: uuid::Uuid,
316        name: String,
317        time: EventTime,
318    }
319
320    let mut db = test.setup_db(models!(Event, EventTime)).await;
321
322    let ts = jiff::Timestamp::from_second(1_700_000_000).unwrap();
323
324    let scheduled = Event::create()
325        .name("launch")
326        .time(EventTime::Scheduled { at: ts })
327        .exec(&mut db)
328        .await?;
329
330    let unscheduled = Event::create()
331        .name("tbd")
332        .time(EventTime::Unscheduled)
333        .exec(&mut db)
334        .await?;
335
336    let found_scheduled = Event::get_by_id(&mut db, &scheduled.id).await?;
337    assert_eq!(found_scheduled.time, EventTime::Scheduled { at: ts });
338
339    let found_unscheduled = Event::get_by_id(&mut db, &unscheduled.id).await?;
340    assert_eq!(found_unscheduled.time, EventTime::Unscheduled);
341
342    Ok(())
343}
344
345#[driver_test]
346pub async fn struct_in_data_variant(test: &mut Test) -> Result<()> {
347    #[derive(Debug, PartialEq, toasty::Embed)]
348    struct Address {
349        street: String,
350        city: String,
351    }
352
353    #[derive(Debug, PartialEq, toasty::Embed)]
354    enum Destination {
355        #[column(variant = 1)]
356        Digital { email: String },
357        #[column(variant = 2)]
358        Physical { address: Address },
359    }
360
361    #[derive(Debug, toasty::Model)]
362    struct Shipment {
363        #[key]
364        #[auto]
365        id: uuid::Uuid,
366        destination: Destination,
367    }
368
369    let mut db = test.setup_db(models!(Shipment, Destination, Address)).await;
370
371    let digital = Shipment::create()
372        .destination(Destination::Digital {
373            email: "user@example.com".to_string(),
374        })
375        .exec(&mut db)
376        .await?;
377
378    let physical = Shipment::create()
379        .destination(Destination::Physical {
380            address: Address {
381                street: "123 Main St".to_string(),
382                city: "Seattle".to_string(),
383            },
384        })
385        .exec(&mut db)
386        .await?;
387
388    let found_digital = Shipment::get_by_id(&mut db, &digital.id).await?;
389    assert_eq!(
390        found_digital.destination,
391        Destination::Digital {
392            email: "user@example.com".to_string()
393        }
394    );
395
396    let found_physical = Shipment::get_by_id(&mut db, &physical.id).await?;
397    assert_eq!(
398        found_physical.destination,
399        Destination::Physical {
400            address: Address {
401                street: "123 Main St".to_string(),
402                city: "Seattle".to_string(),
403            },
404        }
405    );
406
407    Ok(())
408}
409
410/// Roundtrip test for an enum embedded inside a variant field of another enum (enum-in-enum).
411/// The inner enum is unit-only; the outer has one data variant and one unit variant.
412#[driver_test]
413pub async fn enum_in_enum_roundtrip(test: &mut Test) -> Result<()> {
414    #[derive(Debug, PartialEq, toasty::Embed)]
415    enum Channel {
416        #[column(variant = 1)]
417        Email,
418        #[column(variant = 2)]
419        Sms,
420    }
421
422    #[derive(Debug, PartialEq, toasty::Embed)]
423    enum Notification {
424        #[column(variant = 1)]
425        Send { channel: Channel, message: String },
426        #[column(variant = 2)]
427        Suppress,
428    }
429
430    #[derive(Debug, toasty::Model)]
431    struct Alert {
432        #[key]
433        #[auto]
434        id: uuid::Uuid,
435        notification: Notification,
436    }
437
438    let mut db = test.setup_db(models!(Alert, Notification, Channel)).await;
439
440    let a1 = Alert::create()
441        .notification(Notification::Send {
442            channel: Channel::Email,
443            message: "hello".to_string(),
444        })
445        .exec(&mut db)
446        .await?;
447
448    let a2 = Alert::create()
449        .notification(Notification::Send {
450            channel: Channel::Sms,
451            message: "world".to_string(),
452        })
453        .exec(&mut db)
454        .await?;
455
456    let a3 = Alert::create()
457        .notification(Notification::Suppress)
458        .exec(&mut db)
459        .await?;
460
461    let found_a1 = Alert::get_by_id(&mut db, &a1.id).await?;
462    assert_eq!(
463        found_a1.notification,
464        Notification::Send {
465            channel: Channel::Email,
466            message: "hello".to_string(),
467        }
468    );
469
470    let found_a2 = Alert::get_by_id(&mut db, &a2.id).await?;
471    assert_eq!(
472        found_a2.notification,
473        Notification::Send {
474            channel: Channel::Sms,
475            message: "world".to_string(),
476        }
477    );
478
479    let found_a3 = Alert::get_by_id(&mut db, &a3.id).await?;
480    assert_eq!(found_a3.notification, Notification::Suppress);
481
482    Ok(())
483}
484
485/// Verifies field indices are assigned globally across multiple data variants.
486/// With two variants having two fields each, indices should be 0, 1, 2, 3.
487#[driver_test]
488pub async fn global_field_indices(test: &mut Test) {
489    #[allow(dead_code)]
490    #[derive(toasty::Embed)]
491    enum Event {
492        #[column(variant = 1)]
493        Login { user_id: String, ip: String },
494        #[column(variant = 2)]
495        Purchase { item_id: String, amount: i64 },
496    }
497
498    let db = test.setup_db(models!(Event)).await;
499    let schema = db.schema();
500
501    assert_struct!(schema.app.models, #{
502        Event::id(): toasty::schema::app::Model::EmbeddedEnum({
503            fields: [
504                { id.index: 0, name.app: Some("user_id") },
505                { id.index: 1, name.app: Some("ip") },
506                { id.index: 2, name.app: Some("item_id") },
507                { id.index: 3, name.app: Some("amount") },
508            ],
509        }),
510    });
511}