toasty_macros/
lib.rs

1extern crate proc_macro;
2
3mod create;
4
5use proc_macro::TokenStream;
6use quote::quote;
7
8/// Derive macro that turns a struct into a Toasty model backed by a database
9/// table.
10///
11/// For a tutorial-style introduction, see the [Toasty guide].
12///
13#[doc = include_str!(concat!(env!("OUT_DIR"), "/guide_link.md"))]
14///
15/// # Overview
16///
17/// Applying `#[derive(Model)]` to a named struct generates:
18///
19/// - A [`Model`] trait implementation, including the associated `Query`,
20///   `Create`, and `Update` builder types.
21/// - A [`Load`] implementation for deserializing rows from the database.
22/// - A [`Register`] implementation for schema registration at runtime.
23/// - Static query methods such as `all()`, `filter(expr)`,
24///   `filter_by_<field>()`, and `get_by_<key>()`.
25/// - Instance methods `update()` and `delete()`.
26/// - A `Fields` struct returned by `<Model>::fields()` for building typed
27///   filter expressions.
28///
29/// The struct must have named fields and no generic parameters.
30///
31/// [`Model`]: toasty::Model
32/// [`Load`]: toasty::Load
33/// [`Register`]: toasty::Register
34///
35/// # Struct-level attributes
36///
37/// ## `#[key(...)]` — primary key
38///
39/// Defines the primary key at the struct level. Mutually exclusive with
40/// field-level `#[key]`.
41///
42/// **Simple form** — every listed field becomes a partition key:
43///
44/// ```ignore
45/// #[derive(Model)]
46/// #[key(name)]
47/// struct Widget {
48///     name: String,
49///     value: i64,
50/// }
51/// ```
52///
53/// **Composite key with partition/local scoping:**
54///
55/// ```ignore
56/// #[derive(Model)]
57/// #[key(partition = user_id, local = id)]
58/// struct Todo {
59///     #[auto]
60///     id: uuid::Uuid,
61///     user_id: String,
62///     title: String,
63/// }
64/// ```
65///
66/// The `partition` fields determine data distribution (relevant for
67/// DynamoDB); `local` fields scope within a partition. For SQL databases
68/// both behave as a regular composite primary key.
69///
70/// Multiple `partition` and `local` entries are allowed:
71///
72/// ```ignore
73/// #[key(partition = tenant, partition = org, local = id)]
74/// ```
75///
76/// When using named `partition`/`local` syntax, at least one of each is
77/// required. You cannot mix the simple and named forms.
78///
79/// ## `#[table = "name"]` — custom table name
80///
81/// Overrides the default table name. Without this attribute the table name
82/// is the pluralized, snake_case form of the struct name (e.g. `User` →
83/// `users`).
84///
85/// ```ignore
86/// #[derive(Model)]
87/// #[table = "legacy_users"]
88/// struct User {
89///     #[key]
90///     #[auto]
91///     id: i64,
92///     name: String,
93/// }
94/// ```
95///
96/// # Field-level attributes
97///
98/// ## `#[key]` — mark a field as a primary key column
99///
100/// Marks one or more fields as the primary key. When used on multiple
101/// fields each becomes a partition key column (equivalent to listing them
102/// in `#[key(...)]` at the struct level).
103///
104/// Cannot be combined with a struct-level `#[key(...)]` attribute.
105///
106/// ```ignore
107/// #[derive(Model)]
108/// struct User {
109///     #[key]
110///     #[auto]
111///     id: i64,
112///     name: String,
113/// }
114/// ```
115///
116/// ## `#[auto]` — automatic value generation
117///
118/// Tells Toasty to generate this field's value automatically. The strategy
119/// depends on the field type and optional arguments:
120///
121/// | Syntax | Behavior |
122/// |--------|----------|
123/// | `#[auto]` on `uuid::Uuid` | UUID v7 (timestamp-sortable) |
124/// | `#[auto(uuid(v4))]` | UUID v4 (random) |
125/// | `#[auto(uuid(v7))]` | UUID v7 (explicit) |
126/// | `#[auto]` on integer types (`i8`–`i64`, `u8`–`u64`) | Auto-increment |
127/// | `#[auto(increment)]` | Auto-increment (explicit) |
128/// | `#[auto]` on a field named `created_at` | Expands to `#[default(jiff::Timestamp::now())]` |
129/// | `#[auto]` on a field named `updated_at` | Expands to `#[update(jiff::Timestamp::now())]` |
130///
131/// The `created_at`/`updated_at` expansion requires the `jiff` feature and
132/// a field type compatible with `jiff::Timestamp`.
133///
134/// Cannot be combined with `#[default]` or `#[update]` on the same field.
135///
136/// ## `#[default(expr)]` — default value on create
137///
138/// Sets a default value that is used when the field is not explicitly
139/// provided during creation. The expression is any valid Rust expression.
140///
141/// ```ignore
142/// #[default(0)]
143/// view_count: i64,
144///
145/// #[default("draft".to_string())]
146/// status: String,
147/// ```
148///
149/// The default can be overridden by calling the corresponding setter on the
150/// create builder.
151///
152/// Cannot be combined with `#[auto]` on the same field. Can be combined
153/// with `#[update]` (the default applies on create; the update expression
154/// applies on subsequent updates).
155///
156/// ## `#[update(expr)]` — value applied on create and update
157///
158/// Sets a value that Toasty applies every time a record is created or
159/// updated, unless the field is explicitly set on the builder.
160///
161/// ```ignore
162/// #[update(jiff::Timestamp::now())]
163/// updated_at: jiff::Timestamp,
164/// ```
165///
166/// Cannot be combined with `#[auto]` on the same field.
167///
168/// ## `#[index]` — add a database index
169///
170/// Creates a non-unique index on the field. Toasty generates a
171/// `filter_by_<field>` method for indexed fields.
172///
173/// ```ignore
174/// #[index]
175/// email: String,
176/// ```
177///
178/// ## `#[unique]` — add a unique constraint
179///
180/// Creates a unique index on the field. Like `#[index]`, this generates
181/// `filter_by_<field>`. The database enforces uniqueness.
182///
183/// ```ignore
184/// #[unique]
185/// email: String,
186/// ```
187///
188/// ## `#[column(...)]` — customize the database column
189///
190/// Overrides the column name and/or type for a field.
191///
192/// **Custom name:**
193///
194/// ```ignore
195/// #[column("user_email")]
196/// email: String,
197/// ```
198///
199/// **Custom type:**
200///
201/// ```ignore
202/// #[column(type = varchar(255))]
203/// email: String,
204/// ```
205///
206/// **Both:**
207///
208/// ```ignore
209/// #[column("user_email", type = varchar(255))]
210/// email: String,
211/// ```
212///
213/// ### Supported column types
214///
215/// | Syntax | Description |
216/// |--------|-------------|
217/// | `boolean` | Boolean |
218/// | `i8`, `i16`, `i32`, `i64` | Signed integer (1/2/4/8 bytes) |
219/// | `int(N)` | Signed integer with N-byte width |
220/// | `u8`, `u16`, `u32`, `u64` | Unsigned integer (1/2/4/8 bytes) |
221/// | `uint(N)` | Unsigned integer with N-byte width |
222/// | `text` | Unbounded text |
223/// | `varchar(N)` | Text with max length N |
224/// | `numeric` | Arbitrary-precision numeric |
225/// | `numeric(P, S)` | Numeric with precision P and scale S |
226/// | `binary(N)` | Fixed-size binary with N bytes |
227/// | `blob` | Variable-length binary |
228/// | `timestamp(P)` | Timestamp with P fractional-second digits |
229/// | `date` | Date without time |
230/// | `time(P)` | Time with P fractional-second digits |
231/// | `datetime(P)` | Date and time with P fractional-second digits |
232/// | `"custom"` | Arbitrary type string passed through to the driver |
233///
234/// Cannot be used on relation fields.
235///
236/// ## `#[serialize(json)]` — serialize complex types as JSON
237///
238/// Stores the field as a JSON string in the database. Requires the `serde`
239/// feature and that the field type implements `serde::Serialize` and
240/// `serde::Deserialize`.
241///
242/// ```ignore
243/// #[serialize(json)]
244/// tags: Vec<String>,
245/// ```
246///
247/// For `Option<T>` fields, add `nullable` so that `None` maps to SQL
248/// `NULL` rather than the JSON string `"null"`:
249///
250/// ```ignore
251/// #[serialize(json, nullable)]
252/// metadata: Option<HashMap<String, String>>,
253/// ```
254///
255/// Cannot be used on relation fields.
256///
257/// # Relation attributes
258///
259/// ## `#[belongs_to(...)]` — foreign-key reference
260///
261/// Declares a many-to-one (or one-to-one) association through a foreign
262/// key stored on this model.
263///
264/// ```ignore
265/// #[belongs_to(key = user_id, references = id)]
266/// user: toasty::BelongsTo<User>,
267/// ```
268///
269/// | Parameter | Meaning |
270/// |-----------|---------|
271/// | `key = <field>` | Local field holding the foreign key value |
272/// | `references = <field>` | Field on the target model being referenced |
273///
274/// For composite foreign keys, repeat `key`/`references` pairs:
275///
276/// ```ignore
277/// #[belongs_to(key = org_id, references = id, key = tenant_id, references = tenant_id)]
278/// org: toasty::BelongsTo<Org>,
279/// ```
280///
281/// The number of `key` entries must equal the number of `references`
282/// entries.
283///
284/// Wrap the target type in `Option` for an optional (nullable) foreign key:
285///
286/// ```ignore
287/// #[index]
288/// manager_id: Option<i64>,
289///
290/// #[belongs_to(key = manager_id, references = id)]
291/// manager: toasty::BelongsTo<Option<User>>,
292/// ```
293///
294/// ## `#[has_many]` — one-to-many association
295///
296/// Declares a collection of related models. The target model must have a
297/// `#[belongs_to]` field pointing back to this model.
298///
299/// ```ignore
300/// #[has_many]
301/// posts: toasty::HasMany<Post>,
302/// ```
303///
304/// Toasty generates an accessor method (e.g. `.posts()`) and an insert
305/// helper (e.g. `.insert_post()`), where the insert helper name is the
306/// auto-singularized field name.
307///
308/// ### `pair` — disambiguate self-referential or multiple relations
309///
310/// When the target model has more than one `#[belongs_to]` pointing to
311/// the same model (or points to itself), use `pair` to specify which
312/// `belongs_to` field this `has_many` corresponds to:
313///
314/// ```ignore
315/// #[has_many(pair = parent)]
316/// children: toasty::HasMany<Person>,
317/// ```
318///
319/// ## `#[has_one]` — one-to-one association
320///
321/// Declares a single related model. The target model must have a
322/// `#[belongs_to]` field pointing back to this model.
323///
324/// ```ignore
325/// #[has_one]
326/// profile: toasty::HasOne<Profile>,
327/// ```
328///
329/// Wrap in `Option` for an optional association:
330///
331/// ```ignore
332/// #[has_one]
333/// profile: toasty::HasOne<Option<Profile>>,
334/// ```
335///
336/// # Constraints
337///
338/// - The struct must have named fields (tuple structs are not supported).
339/// - Generic parameters are not supported.
340/// - Every root model must have a primary key, defined either by a
341///   struct-level `#[key(...)]` or by one or more field-level `#[key]`
342///   attributes, but not both.
343/// - `#[auto]` cannot be combined with `#[default]` or `#[update]` on the
344///   same field.
345/// - `#[column]`, `#[default]`, `#[update]`, and `#[serialize]` cannot be
346///   used on relation fields (`BelongsTo`, `HasMany`, `HasOne`).
347/// - A field can have at most one relation attribute.
348/// - `Self` can be used as a type in relation fields for self-referential
349///   models.
350///
351/// # Full example
352///
353/// ```ignore
354/// #[derive(Debug, toasty::Model)]
355/// struct User {
356///     #[key]
357///     #[auto]
358///     id: i64,
359///
360///     #[unique]
361///     email: String,
362///
363///     name: String,
364///
365///     #[default(jiff::Timestamp::now())]
366///     created_at: jiff::Timestamp,
367///
368///     #[update(jiff::Timestamp::now())]
369///     updated_at: jiff::Timestamp,
370///
371///     #[has_many]
372///     posts: toasty::HasMany<Post>,
373/// }
374///
375/// #[derive(Debug, toasty::Model)]
376/// struct Post {
377///     #[key]
378///     #[auto]
379///     id: i64,
380///
381///     title: String,
382///
383///     #[serialize(json)]
384///     tags: Vec<String>,
385///
386///     #[index]
387///     user_id: i64,
388///
389///     #[belongs_to(key = user_id, references = id)]
390///     user: toasty::BelongsTo<User>,
391/// }
392/// ```
393#[proc_macro_derive(
394    Model,
395    attributes(
396        key, auto, default, update, column, index, unique, table, has_many, has_one, belongs_to,
397        serialize
398    )
399)]
400pub fn derive_model(input: TokenStream) -> TokenStream {
401    match toasty_codegen::generate_model(input.into()) {
402        Ok(output) => output.into(),
403        Err(e) => e.to_compile_error().into(),
404    }
405}
406
407#[proc_macro_derive(Embed, attributes(column, index, unique))]
408pub fn derive_embed(input: TokenStream) -> TokenStream {
409    match toasty_codegen::generate_embed(input.into()) {
410        Ok(output) => output.into(),
411        Err(e) => e.to_compile_error().into(),
412    }
413}
414
415#[proc_macro]
416pub fn include_schema(_input: TokenStream) -> TokenStream {
417    todo!()
418}
419
420#[proc_macro]
421pub fn query(_input: TokenStream) -> TokenStream {
422    quote!(println!("TODO")).into()
423}
424
425/// Expands struct-literal syntax into create builder method chains. Returns one
426/// or more create builders — call `.exec(&mut db).await?` to insert the
427/// record(s).
428///
429/// # Syntax forms
430///
431/// ## Single creation
432///
433/// ```ignore
434/// toasty::create!(Type { field: value, ... })
435/// ```
436///
437/// Expands to `Type::create().field(value)...` and returns the model's create
438/// builder (e.g., `UserCreate`).
439///
440/// ```ignore
441/// let user = toasty::create!(User {
442///     name: "Alice",
443///     email: "alice@example.com"
444/// })
445/// .exec(&mut db)
446/// .await?;
447/// ```
448///
449/// ## Scoped creation
450///
451/// ```ignore
452/// toasty::create!(in expr { field: value, ... })
453/// ```
454///
455/// Expands to `expr.create().field(value)...`. Creates a record through a
456/// relation accessor. The foreign key is set automatically.
457///
458/// ```ignore
459/// let todo = toasty::create!(in user.todos() { title: "buy milk" })
460///     .exec(&mut db)
461///     .await?;
462///
463/// // todo.user_id == user.id
464/// ```
465///
466/// ## Typed batch
467///
468/// ```ignore
469/// toasty::create!(Type::[ { fields }, { fields }, ... ])
470/// ```
471///
472/// Expands to a tuple of create builders. Pass the result to
473/// [`toasty::batch()`] to execute:
474///
475/// ```ignore
476/// let (alice, bob) = toasty::batch(toasty::create!(User::[
477///     { name: "Alice" },
478///     { name: "Bob" },
479/// ]))
480/// .exec(&mut db)
481/// .await?;
482/// ```
483///
484/// ## Mixed batch
485///
486/// ```ignore
487/// toasty::create!([
488///     Type { fields },
489///     in expr { fields },
490///     ...
491/// ])
492/// ```
493///
494/// Expands to a tuple of create builders, one per item. Each item can be a
495/// typed creation or a scoped creation.
496///
497/// # Field values
498///
499/// ## Expressions
500///
501/// Any Rust expression is valid as a field value — literals, variables, and
502/// function calls all work.
503///
504/// ```ignore
505/// let name = "Alice";
506/// toasty::create!(User { name: name, email: format!("{}@example.com", name) })
507/// ```
508///
509/// ## Nested struct (BelongsTo / HasOne)
510///
511/// Use `{ ... }` **without** a type prefix to create a related record inline.
512/// The macro calls the `with_<field>` closure setter on the builder.
513///
514/// ```ignore
515/// toasty::create!(Todo {
516///     title: "buy milk",
517///     user: { name: "Alice" }
518/// })
519/// // Expands to:
520/// // Todo::create()
521/// //     .title("buy milk")
522/// //     .with_user(|b| { let b = b.name("Alice"); b })
523/// ```
524///
525/// The related record is created first and the foreign key is set
526/// automatically.
527///
528/// ## Nested list (HasMany)
529///
530/// Use `[{ ... }, { ... }]` to create multiple related records. The macro calls
531/// `with_<field>` with a `CreateMany` builder, invoking `.with_item()` for each
532/// entry.
533///
534/// ```ignore
535/// toasty::create!(User {
536///     name: "Alice",
537///     todos: [{ title: "first" }, { title: "second" }]
538/// })
539/// // Expands to:
540/// // User::create()
541/// //     .name("Alice")
542/// //     .with_todos(|b| b
543/// //         .with_item(|b| { let b = b.title("first"); b })
544/// //         .with_item(|b| { let b = b.title("second"); b })
545/// //     )
546/// ```
547///
548/// Items in a nested list can also be plain expressions (e.g., an existing
549/// builder value).
550///
551/// ## Deep nesting
552///
553/// Nesting composes to arbitrary depth:
554///
555/// ```ignore
556/// toasty::create!(User {
557///     name: "Alice",
558///     todos: [{
559///         title: "task",
560///         tags: [{ name: "urgent" }, { name: "work" }]
561///     }]
562/// })
563/// ```
564///
565/// This creates a `User`, then a `Todo` linked to that user, then two `Tag`
566/// records linked to that todo.
567///
568/// # Fields that can be omitted
569///
570/// | Field type | Behavior when omitted |
571/// |---|---|
572/// | `#[auto]` | Value generated by the database or Toasty |
573/// | `Option<T>` | Defaults to `None` (`NULL`) |
574/// | `#[default(expr)]` | Uses the default expression |
575/// | `#[update(expr)]` | Uses the expression as the initial value |
576/// | `HasMany<T>` | No related records created |
577/// | `HasOne<Option<T>>` | No related record created |
578/// | `BelongsTo<Option<T>>` | Foreign key set to `NULL` |
579///
580/// Required fields (`String`, `i64`, non-optional `BelongsTo`, etc.) that are
581/// missing do not cause a compile-time error. The insert fails at runtime with
582/// a database constraint violation.
583///
584/// # Compile errors
585///
586/// **Type prefix on nested struct:**
587///
588/// ```ignore
589/// // Error: remove the type prefix `User` — use `{ ... }` without a type name
590/// toasty::create!(Todo { user: User { name: "Alice" } })
591///
592/// // Correct:
593/// toasty::create!(Todo { user: { name: "Alice" } })
594/// ```
595///
596/// Nested struct values infer their type from the field.
597///
598/// **Nested lists:**
599///
600/// ```ignore
601/// // Error: nested lists are not supported in create!
602/// toasty::create!(User { field: [[{ ... }]] })
603/// ```
604///
605/// **Missing braces or batch bracket:**
606///
607/// ```ignore
608/// // Error: expected `{` for single creation or `::[` for batch creation after type path
609/// toasty::create!(User)
610/// ```
611///
612/// # Return type
613///
614/// | Form | Returns |
615/// |---|---|
616/// | `Type { ... }` | `TypeCreate` (single builder) |
617/// | `in expr { ... }` | Builder for the relation's model |
618/// | `Type::[ ... ]` | Tuple of `TypeCreate` builders |
619/// | `[ ... ]` | Tuple of builders (one per item) |
620///
621/// None of the forms execute the insert on their own. Call
622/// `.exec(&mut db).await?` on a single builder, or pass batch tuples to
623/// [`toasty::batch()`].
624#[proc_macro]
625pub fn create(input: TokenStream) -> TokenStream {
626    match create::generate(input.into()) {
627        Ok(output) => output.into(),
628        Err(e) => e.to_compile_error().into(),
629    }
630}