siderite_macros/lib.rs
1//! Procedural macros for siderite.
2//!
3//! These macros are re-exported by the `siderite` facade crate; generated code
4//! only refers to `::siderite::...` paths, so depend on `siderite`, not on this
5//! crate directly.
6//!
7//! * `#[get("/path", ...)]`, `#[post(..)]`, `#[put(..)]`, `#[patch(..)]`,
8//! `#[delete(..)]`, `#[head(..)]`, `#[options(..)]`, `#[ws(..)]`: attach a
9//! route (and its OpenAPI metadata) to an `async fn` handler.
10//! * `routes![a, module::b]`: collect the routes generated by those attributes.
11//! * `#[derive(Schema)]`: JSON Schema / OpenAPI schema generation compatible
12//! with the common `serde` attributes, plus the matching `Dump`.
13//! * `#[derive(Validate)]`: request validation (coercion, constraints,
14//! aliases, string transforms) generated from the same attributes.
15//! * `#[derive(Model)]`: ORM model metadata, typed field handles, the `Model`
16//! implementation and relation accessors.
17//! * `#[model_hooks]`: field/model validators, computed fields and
18//! serializers on an inherent `impl` block.
19#![forbid(unsafe_code)]
20
21mod attrs;
22mod diag;
23mod docs;
24mod generics;
25mod hooks;
26mod meta;
27mod model;
28mod probe;
29mod receiver;
30mod route;
31mod routes;
32mod schema;
33mod validate;
34
35use proc_macro::TokenStream;
36
37macro_rules! route_macros {
38 ($($(#[$doc:meta])* $name:ident => $method:ident),+ $(,)?) => {$(
39 $(#[$doc])*
40 #[proc_macro_attribute]
41 pub fn $name(args: TokenStream, item: TokenStream) -> TokenStream {
42 route::expand(route::Method::$method, args.into(), item.into()).into()
43 }
44 )+};
45}
46
47route_macros! {
48 /// Route `GET` requests to the annotated `async fn`.
49 ///
50 /// Syntax: `#[get("/path/{param}", status = 200, tag = "t", tags("a", "b"),
51 /// summary = "..", description = "..", operation_id = "..",
52 /// response_model = Type, deprecated, hidden)]`.
53 get => Get,
54 /// Route `POST` requests to the annotated `async fn`. See [`macro@get`].
55 post => Post,
56 /// Route `PUT` requests to the annotated `async fn`. See [`macro@get`].
57 put => Put,
58 /// Route `PATCH` requests to the annotated `async fn`. See [`macro@get`].
59 patch => Patch,
60 /// Route `DELETE` requests to the annotated `async fn`. See [`macro@get`].
61 delete => Delete,
62 /// Route `HEAD` requests to the annotated `async fn`. See [`macro@get`].
63 head => Head,
64 /// Route `OPTIONS` requests to the annotated `async fn`. See [`macro@get`].
65 options => Options,
66 /// Route a WebSocket upgrade (a `GET` request) to the annotated `async fn`.
67 /// See [`macro@get`].
68 ws => Ws,
69}
70
71/// Collect the routes of handlers annotated with the route attributes.
72///
73/// `routes![create_user, users::list]` expands to
74/// `::std::vec![__siderite_route_create_user(), users::__siderite_route_list()]`
75/// (only the last path segment is rewritten), suitable for `App::routes`.
76#[proc_macro]
77pub fn routes(input: TokenStream) -> TokenStream {
78 routes::expand(input.into()).into()
79}
80
81/// Derive `siderite::validation::Schema` (JSON Schema / OpenAPI 3.1) and the
82/// matching `siderite::validation::Dump`.
83///
84/// Helper attributes: `#[schema(name = "..", inline, no_dump)]`,
85/// `#[field(...)]`, `#[model_config(...)]` and the compatible subset of
86/// `#[serde(...)]`. See [`macro@Validate`] for the shared attributes.
87///
88/// `Dump` is implemented for every derived type, but the impl only applies
89/// when the type is `Serialize` (input-only types are unaffected). Add
90/// `#[schema(no_dump)]` to opt out.
91#[proc_macro_derive(Schema, attributes(schema, field, serde, model_config))]
92pub fn derive_schema(input: TokenStream) -> TokenStream {
93 let input = syn::parse_macro_input!(input as syn::DeriveInput);
94 match schema::derive(&input) {
95 Ok(tokens) => tokens.into(),
96 Err(err) => err.into_compile_error().into(),
97 }
98}
99
100/// Derive `siderite::validation::Validate`.
101///
102/// Serde stays the single source of truth for wire names, defaults and
103/// skipping; `derive(Schema)`, `derive(Validate)` and the generated `Dump`
104/// share one attribute model, so key names and required-ness cannot drift.
105///
106/// # `#[model_config(...)]`
107/// `strict`, `extra = "forbid" | "ignore" | "allow"`, `str_strip_whitespace`,
108/// `str_to_lower`, `str_to_upper`, `populate_by_name` (the Rust field name
109/// is accepted as an input key too) and `hooks` (see below). Flags accept
110/// `flag` and `flag = true`. Serde's `deny_unknown_fields` implies
111/// `extra = "forbid"` unless `extra` is given. As with `deny_unknown_fields`,
112/// the key of a `#[serde(skip_deserializing)]` (or `skip`) field is not an
113/// input, so under `extra = "forbid"` sending it is `extra_forbidden`.
114///
115/// # `#[field(...)]`
116/// Constraints: `min_length`, `max_length`, `pattern` (or `regex`, checked at
117/// compile time), `email`, `gt`, `ge`, `lt`, `le`, `multiple_of` (numeric
118/// literals). Naming: `alias` (must equal the serde key; use
119/// `#[serde(rename)]` to rename), `validation_alias` (extra accepted input
120/// key), `serialization_alias` (must equal the serde key: there is one schema
121/// per type, unlike Pydantic). Defaults: `default = <expr>` /
122/// `default_factory = path` document the default and drive
123/// `exclude_defaults`; the field must *also* have a serde default
124/// (`#[serde(default)]`, `#[serde(default = "path")]`, or a container
125/// default), which stays the source of truth for deserialization.
126/// Annotations: `title`, `description`, `examples(..)`, `url`,
127/// `max_digits`/`decimal_places` (documented as `x-` schema extensions,
128/// not enforced). Behaviour: `exclude` (skipped by `Dump`, input still
129/// accepted), `strict` (strict coercion for this field only) and
130/// `validator = path` (repeatable reusable after-validator
131/// `fn(&FieldTy) -> Result<(), FieldError>`). Unknown keys are compile
132/// errors listing the valid ones.
133///
134/// # Supported shapes
135/// * Structs with named fields: full validation.
136/// * Newtype structs delegate to the inner type.
137/// * Enums with only unit variants (default representation) check that the
138/// input is one of the serde names (error code `enum`).
139/// * Other enums and tuple/unit structs implement `Validate` without
140/// validating anything themselves.
141///
142/// # Hooks and generics
143/// Hooks defined with [`macro@model_hooks`] are found automatically for
144/// concrete types. For **generic** types the derive cannot detect a
145/// `ModelHooks` impl, so it emits no hook calls unless the type says
146/// `#[model_config(hooks)]`, which calls `<Self as ModelHooks>` directly (an
147/// impl is then required, even if empty). Type parameters get a `Validate`
148/// bound.
149#[proc_macro_derive(Validate, attributes(schema, field, serde, model_config))]
150pub fn derive_validate(input: TokenStream) -> TokenStream {
151 let input = syn::parse_macro_input!(input as syn::DeriveInput);
152 match validate::derive(&input) {
153 Ok(tokens) => tokens.into(),
154 Err(err) => err.into_compile_error().into(),
155 }
156}
157
158/// Turn an inherent `impl` block into a `ModelHooks` implementation.
159///
160/// Helper attributes on the methods (removed from the emitted impl):
161///
162/// * `#[field_validator("a", "b", mode = "after")]`:
163/// `fn(value: &FieldTy) -> Result<(), FieldError>`, run on the typed value
164/// at the field's input key; `&String` coerces to `&str`. With
165/// `mode = "before"`: `fn(value: &mut serde_json::Value)`, run on the raw
166/// slot before the field is coerced. Field names are resolved as real
167/// fields, so a typo is a compile error. Requires `derive(Validate)`.
168/// * `#[model_validator(mode = "after")]`: `fn(&self) -> Result<(), FieldError>`;
169/// `mode = "before"`: `fn(input: &mut Value) -> Result<(), FieldError>`.
170/// Errors are located at the model unless the `FieldError` has a location.
171/// * `#[computed_field]` / `#[computed_field(alias = "name")]`:
172/// `fn(&self) -> T` with `T: Dump + Schema` (an explicit type, not
173/// `impl Trait`). Included by `Dump` (respecting include/exclude) and
174/// documented as a `readOnly` property.
175/// * `#[field_serializer("field")]`: `fn(value: &FieldTy) -> V`, `V:
176/// Serialize` (`serde_json::Value` included), replaces the field's dumped
177/// value.
178/// * `#[model_serializer]`: `fn(&self, value: Value) -> Value`, applied to
179/// the finished object.
180///
181/// Hooks run in declaration order. Generic impls are supported (the
182/// generics and where clause are copied); the model then needs
183/// `#[model_config(hooks)]` and the impl's bounds must be satisfiable where
184/// the model is used.
185#[proc_macro_attribute]
186pub fn model_hooks(args: TokenStream, item: TokenStream) -> TokenStream {
187 hooks::expand(args.into(), item.into()).into()
188}
189
190/// Derive `siderite::orm::Model`: static column metadata, one typed
191/// `Field<Self, T>` constant per field (`User::name`), `to_values` /
192/// `from_row`, and the relation accessors.
193///
194/// One struct can derive `Model, Validate, Schema, Serialize, Deserialize`
195/// together: the ORM keys of `#[field(...)]` are ignored by the other derives,
196/// and `Model` ignores their validation keys (except `max_length`,
197/// `max_digits` and `decimal_places`, which become column metadata).
198///
199/// # `#[model(...)]` on the struct
200/// * `table = "users"`: table name. Default: the snake_case of the type
201/// name (`BlogPost` becomes `blog_post`); there is no app prefix or
202/// pluralisation.
203/// * `ordering = ["name", "-created_at"]`: default ordering; a leading `-`
204/// is descending. Entries are field names (or column names).
205/// * `indexes(idx_name(columns = ["a", "b"], unique))`: named indexes.
206/// * `unique_together(["a", "b"], ["c", "d"])`: unique constraints, named
207/// `{table}_{columns}_uniq`.
208/// * `checks(age_positive = "age >= 0")`: named `CHECK` constraints. The SQL
209/// is written by the model author, never built from user input.
210/// * `managed = false`: migrations leave the table alone.
211/// * `many_to_many(tags(Tag, through_table = "post_tags", source_column = "..",
212/// target_column = "..", related_name = "posts"))`: many-to-many relations
213/// (not struct fields). Defaults: `through_table` is `{table}_{name}`,
214/// `source_column` is `{snake(Model)}_id` and `target_column` is
215/// `{snake(Target)}_id` (set both explicitly for a self-referential
216/// relation). `through = Model` uses an explicit through model: its table
217/// is the join table (`through_table` must then be omitted).
218///
219/// Indexes, unique groups and ordering are checked at compile time: unknown
220/// fields are errors.
221///
222/// # `#[field(...)]` ORM keys
223/// * `primary_key`: exactly one field must have it. Without a declared key
224/// the derive suggests `#[field(primary_key, auto)] id: i64`.
225/// * `auto`: database-generated integer primary key (`i16`, `i32`, `i64`).
226/// The model is unsaved while the key equals `Default::default()`; a
227/// primary key without `auto` is never "unsaved".
228/// * `unique`, `index`: constraint and single-column index.
229/// * `column = "name"`: column name (default: the field name, or
230/// `{field}_id` for relations).
231/// * `db_default = <int | bool | "text">`: server-side default.
232/// * `auto_now_add` / `auto_now`: `DbDefault::Now` at insert / refresh on
233/// every save (the queryset layer fills the value; the derive only emits
234/// metadata).
235/// * `on_delete = "cascade" | "protect" | "set_null" | "set_default" |
236/// "do_nothing"` and `related_name = ".."`: relation fields only.
237/// `set_null` requires `Option<ForeignKey<..>>`.
238/// * `skip`: not a column; the field is `Default::default()` when loaded.
239/// * `max_length`, `max_digits`, `decimal_places`: column metadata.
240///
241/// # Relations
242/// Fields typed `ForeignKey<T>`, `OneToOne<T>` or `Option<..>` of those are
243/// relations; a `OneToOne` is unique. The field's constant is a
244/// `Field<Self, ForeignKey<T>>` over the `{field}_id` column, and an async
245/// `fetch_{field}(&self, &Db)` loads the target (the name differs from the
246/// field's because an associated constant and a method cannot share a name).
247/// With `related_name`, the target type gets a reverse accessor
248/// (`author.books(&db)` returning a `QuerySet`, or an async `Option` lookup
249/// for a `OneToOne`); the target model must be defined in the same crate.
250/// Each many-to-many gets `post.tags(&db)` returning a
251/// `ManyToManyManager`, and with `related_name` a reverse
252/// `tag.posts(&db)` queryset.
253///
254/// Generated accessors can clash with methods you write yourself; the
255/// compiler reports the duplicate.
256///
257/// Generic types are not supported.
258#[proc_macro_derive(Model, attributes(model, field, serde))]
259pub fn derive_model(input: TokenStream) -> TokenStream {
260 let input = syn::parse_macro_input!(input as syn::DeriveInput);
261 match model::derive(&input) {
262 Ok(tokens) => tokens.into(),
263 Err(err) => err.into_compile_error().into(),
264 }
265}
266
267/// Turn an `async fn` into an ORM signal receiver:
268/// `#[receiver(post_save, model = User)]`.
269///
270/// Generates `fn <name>_receiver() -> ::siderite::orm::signals::Receiver`;
271/// register it explicitly with `Signals::connect`. See the ORM signals
272/// documentation for ordering and failure semantics.
273#[proc_macro_attribute]
274pub fn receiver(args: TokenStream, item: TokenStream) -> TokenStream {
275 receiver::expand(args.into(), item.into()).into()
276}