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