kynos_macros/lib.rs
1//! Procedural macros for the Kynos REST API framework.
2//!
3//! Nothing here is meant to be used directly: every macro is re-exported from
4//! `kynos`, and the documentation lives next to the trait each one implements.
5//!
6//! # Why the route attributes exist
7//!
8//! Kynos deliberately has no attribute DSL restating a handler's signature.
9//! utoipa's `#[utoipa::path(responses(...))]` is written by hand beside the
10//! code it describes, and nothing keeps the two in step — which is the single
11//! most common way a generated OpenAPI document ends up wrong.
12//!
13//! The route attributes therefore carry only what the types cannot: the method,
14//! the path, and prose. Parameters come from the handler's arguments, responses
15//! from its return type, and neither is restated anywhere.
16//!
17//! What the attribute *does* add is compile-time checking the builder form
18//! cannot do — chiefly that a path template's variables match the handler's
19//! path parameters.
20//!
21//! # Why the examples here are `ignore`d
22//!
23//! Every expansion names `::kynos::…`, which this crate cannot depend on, so a
24//! doctest here would not compile whatever the derive emitted. The compiled
25//! demonstrations live in `crates/kynos/tests/derives.rs` and the framework's
26//! examples; `AGENTS.md` records the carve-out.
27
28#[cfg(feature = "assets")]
29mod assets;
30mod derive;
31mod route;
32
33use proc_macro::TokenStream;
34
35/// Declares a `GET` operation.
36///
37/// ```ignore
38/// /// Fetch a single user.
39/// ///
40/// /// The first line becomes the operation's summary, the rest its description.
41/// #[kynos::get("/users/{id}", catch_panics)]
42/// async fn get_user(Path(id): Path<UserId>) -> Result<Json<User>, ApiError> {
43/// todo!()
44/// }
45/// ```
46///
47/// `catch_panics` installs a compile-time-selected recovery boundary for this
48/// operation and contributes its 500 response. It is a compile-time error to
49/// use it when the final binary is built with `panic = "abort"`.
50///
51/// Accepts `operation_id = "..."` and `tag = SomeTag` after the path. The tag
52/// becomes `EndpointMeta::TAGS`, which is what puts it in the description; it
53/// may be named once, since `Router::tag`, `Group::tag` and
54/// `EndpointBuilder::tag` are how an operation acquires the rest.
55#[proc_macro_attribute]
56pub fn get(attribute: TokenStream, item: TokenStream) -> TokenStream {
57 route::expand("GET", attribute, item)
58}
59
60/// Declares a `POST` operation. See [`macro@get`] for the syntax.
61#[proc_macro_attribute]
62pub fn post(attribute: TokenStream, item: TokenStream) -> TokenStream {
63 route::expand("POST", attribute, item)
64}
65
66/// Declares a `PUT` operation. See [`macro@get`] for the syntax.
67#[proc_macro_attribute]
68pub fn put(attribute: TokenStream, item: TokenStream) -> TokenStream {
69 route::expand("PUT", attribute, item)
70}
71
72/// Declares a `PATCH` operation. See [`macro@get`] for the syntax.
73#[proc_macro_attribute]
74pub fn patch(attribute: TokenStream, item: TokenStream) -> TokenStream {
75 route::expand("PATCH", attribute, item)
76}
77
78/// Declares a `DELETE` operation. See [`macro@get`] for the syntax.
79#[proc_macro_attribute]
80pub fn delete(attribute: TokenStream, item: TokenStream) -> TokenStream {
81 route::expand("DELETE", attribute, item)
82}
83
84/// Declares a `HEAD` operation. See [`macro@get`] for the syntax.
85///
86/// Rarely needed: a `HEAD` is answered from the corresponding `GET` unless one
87/// is declared, per RFC 9110. Declare it only when the description should say
88/// so explicitly.
89#[proc_macro_attribute]
90pub fn head(attribute: TokenStream, item: TokenStream) -> TokenStream {
91 route::expand("HEAD", attribute, item)
92}
93
94/// Declares an `OPTIONS` operation. See [`macro@get`] for the syntax.
95///
96/// CORS preflight is handled without one: where a `Cors` interceptor covers a
97/// path, the router registers a preflight answer on it while the service is
98/// built. Declare this only for an `OPTIONS` that is part of the API's own
99/// contract.
100///
101/// Declaring one *suppresses* the synthesized preflight on that path — a
102/// hand-written operation wins, and it then owns answering preflights there too.
103#[proc_macro_attribute]
104pub fn options(attribute: TokenStream, item: TokenStream) -> TokenStream {
105 route::expand("OPTIONS", attribute, item)
106}
107
108/// Declares a `TRACE` operation. See [`macro@get`] for the syntax.
109#[proc_macro_attribute]
110pub fn trace(attribute: TokenStream, item: TokenStream) -> TokenStream {
111 route::expand("TRACE", attribute, item)
112}
113
114/// Declares a `QUERY` operation.
115///
116/// Requires the `openapi32` feature: `QUERY` has no Path Item field before
117/// OpenAPI 3.2.
118#[cfg(feature = "openapi32")]
119#[proc_macro_attribute]
120pub fn query(attribute: TokenStream, item: TokenStream) -> TokenStream {
121 route::expand("QUERY", attribute, item)
122}
123
124/// Declares an operation for a method with no dedicated attribute.
125///
126/// ```ignore
127/// #[kynos::operation(method = "PROPFIND", path = "/files/{id}")]
128/// async fn propfind(Path(id): Path<FileId>) -> Result<Json<Properties>, ApiError> {
129/// todo!()
130/// }
131/// ```
132///
133/// A method outside the eight OpenAPI 3.1 names emits into
134/// `additionalOperations`, which requires the `openapi32` feature.
135#[proc_macro_attribute]
136pub fn operation(attribute: TokenStream, item: TokenStream) -> TokenStream {
137 route::expand_generic(attribute, item)
138}
139
140/// Collects operations for mounting.
141///
142/// Operations sharing a path are merged into one Path Item, so `routes![list,
143/// create]` on `/users` produces a single entry with `get` and `post`.
144///
145/// ```ignore
146/// Router::new().mount(routes![users::list, users::create, users::get]);
147/// ```
148#[proc_macro]
149pub fn routes(input: TokenStream) -> TokenStream {
150 route::routes::expand_routes(input)
151}
152
153/// A path template validated at compile time.
154///
155/// ```ignore
156/// let template = path!("/users/{id}");
157/// ```
158///
159/// Rejects a template that does not start with `/`, has unbalanced braces,
160/// repeats a variable, or carries a query string — none of which are legal as
161/// a Paths key.
162#[proc_macro]
163pub fn path(input: TokenStream) -> TokenStream {
164 route::path::expand_path(input)
165}
166
167/// Compiles a directory into the binary as a described asset set.
168///
169/// ```ignore
170/// kynos::assets! {
171/// /// The built single-page app.
172/// pub struct Site;
173/// dir = "dist",
174/// exclude = [".map"],
175/// warn_over = "4MiB",
176/// }
177/// ```
178///
179/// `dir` resolves against `CARGO_MANIFEST_DIR`. Every file becomes an
180/// `include_bytes!`, so changing one rebuilds the crate; *adding* or *removing*
181/// one does not, which a `cargo::rerun-if-changed` line in `build.rs` closes.
182///
183/// A file whose name no path template can express is a compile error naming it,
184/// because a static asset Kynos cannot describe is one it will not serve.
185/// Dotfiles and symlinks are skipped: the first keeps `.git` out of a binary,
186/// the second keeps a set inside its own directory.
187///
188/// An embedded set past 2 MiB emits a compiler warning at the `dir` literal.
189/// Raise the threshold with `warn_over = "8MiB"`, or turn it off with
190/// `warn_over = "none"`. Under `-D warnings` it becomes an error, which is
191/// arguably right and is exactly why the override exists.
192#[cfg(feature = "assets")]
193#[proc_macro]
194pub fn assets(item: TokenStream) -> TokenStream {
195 assets::expand(item)
196}
197
198/// Describes a type as JSON Schema.
199///
200/// Reads the serde attributes already on the type — `rename_all`, `skip`,
201/// `flatten`, `alias`, `tag`, `content`, `transparent`, `deny_unknown_fields` —
202/// so the schema and the wire form come from one declaration. A named field
203/// serde reads under an `alias` is a property under each name it reads, present
204/// under exactly one where it is required and under at most one otherwise,
205/// since serde refuses a document naming two. A variant serde reads under an
206/// `alias` is named under each name it reads: in the `enum` of an all-unit
207/// enum, in the `enum` a tag or an externally tagged unit variant then is in
208/// place of a `const`, and as a property of an externally tagged object branch,
209/// present under exactly one. A name two variants claim is named under the
210/// first alone, the one serde reads it as. Under
211/// `deny_unknown_fields`, every object serde then refuses unknown keys in is
212/// closed: a struct, each struct variant's fields, and an adjacently tagged
213/// branch. The derive uses `additionalProperties: false`, or
214/// `unevaluatedProperties: false` where the object carries an `allOf`, which a
215/// flattened field composes members through and an aliased field bounds its
216/// names in. An externally tagged branch that is an object admits
217/// only its variant key, with or without the attribute, since serde reads it as
218/// exactly one entry. A shape that closes does not implement `Flatten`, and
219/// neither does any externally tagged enum. A flattened field of an object the
220/// attribute closes is also bounded by the narrower `ClosedFlatten`, below. A
221/// field is left out of `required` when it is an `Option`, carries
222/// `#[serde(default)]`, or belongs to a struct carrying
223/// `#[serde(default)]`, because the wire form then allows it to be absent both
224/// ways. A named field serde reads and never writes, `skip_serializing` alone,
225/// is described under that same rule, and one serde writes and never reads,
226/// `skip_deserializing` alone, is left out. In an object serde writes,
227/// `skip_serializing_if` on a field serde reads is accepted only alongside an
228/// `Option` or a `#[serde(default)]`, or on a flattened `#[schema(open)]` map,
229/// which serde reads absent as empty and `required` never lists. A field of a
230/// variant serde never writes, `skip_serializing` on the variant, is only read,
231/// so it may carry `skip_serializing_if` without any of them. A `transparent`
232/// struct is described by the one field serde writes and reads through, or the
233/// single field of the one direction serde can derive for it,
234/// keeps its own component name, as a newtype does, and carries that field's
235/// constraints. A named struct's `tag` is one more required property, whose
236/// `const` is the struct's serde name: its container `rename`, the serialize
237/// side where the rename is split, otherwise its identifier, which `rename_all`
238/// does not reach. It carries no `discriminator`, a struct having no branches
239/// to tell apart, and a `transparent` struct writes no tag. A tuple is the array of the members serde does not skip both
240/// ways, and a newtype variant whose member serde skips is the unit variant
241/// serde writes, provided serde also reads it back. A newtype, and each
242/// described member of a tuple, tuple variant or newtype variant, carries its
243/// constraints, prose and `#[deprecated]` as a named field does. A variant
244/// serde skips both ways is described nowhere, and one serde reads and never
245/// writes is described as serde reads it, under its prose and `#[deprecated]`.
246///
247/// Constraints go on fields, named or unnamed, and the grammar is exactly the
248/// keys of
249/// [`Constraints`](https://docs.rs/kynos/latest/kynos/schema/constraints/struct.Constraints.html)
250/// so that
251/// the attribute and the type it fills cannot drift: `minimum`, `maximum`,
252/// `exclusive_minimum`, `exclusive_maximum`, `multiple_of`, `min_length`,
253/// `max_length`, `pattern`, `min_items`, `max_items` and the `unique_items`
254/// flag. They become JSON Schema assertions *and* the parser's checks, which is
255/// what keeps the description honest without a JSON Schema interpreter on the
256/// hot path.
257///
258/// `format` is **not** among them. It states what a value *is*, which follows
259/// from the type or from nothing, so a `String` annotated as a UUID is a
260/// compile error naming the remedy — `uuid::Uuid`, one of the date, time or
261/// decimal types behind their features, or a newtype with its own `Schema`.
262/// A constraint on one field is `pattern`; a claim about a type is the type's.
263///
264/// `open` is the one member of `#[schema(...)]` that is not a constraint. It
265/// goes on a `#[serde(flatten)]` field to say that the object really does admit
266/// members nothing names, which is the only thing a flattened map can mean. The
267/// field's type must implement
268/// [`OpenMap`](https://docs.rs/kynos/latest/kynos/schema/flatten/trait.OpenMap.html)
269/// — a `HashMap`, a `BTreeMap` or an `Unchecked` over a map, not a type that
270/// refers to one — and a key type's `propertyNames` does not survive it.
271///
272/// # Rejected, because serde and the schema would disagree
273///
274/// - `#[serde(with = ...)]`, `serialize_with`, `deserialize_with` on a field or
275/// a variant the schema describes. The wire form no longer follows from the
276/// Rust type, so a schema derived from the Rust type would be a lie. Give the
277/// value a newtype whose own `Serialize`, `Deserialize` and `Schema` agree on
278/// that form instead. Exempt, being in no schema, are a named field serde
279/// never reads, a variant serde skips both ways and its fields, and a member
280/// of a tuple struct, tuple variant or newtype variant serde skips both ways;
281/// a newtype struct's member never is, since serde writes it through the
282/// function whatever it skips. A flattened `PhantomData` serde reads is in no
283/// schema and still refused, since serde hands the function the parent object
284/// to write members into and read members from. Where serde never writes (a
285/// variant carrying `skip_serializing`, its fields, and a named field carrying
286/// `skip_serializing` alone) only `with` and `deserialize_with` are refused. A
287/// `transparent` struct is checked only on the field serde picks in each
288/// direction: `with` and `serialize_with` on the one field it writes through,
289/// `with` and `deserialize_with` on the one field it reads through, and
290/// nothing for a direction with no single candidate, since serde then refuses
291/// that direction's derive.
292/// - `#[serde(untagged)]` enums. `anyOf` with no discriminator is ambiguous to
293/// decode, and the tie-break is inexpressible. Use an internally or
294/// adjacently tagged enum, which becomes a `discriminator`. The same holds for
295/// one untagged variant serde reads or writes: it goes on the wire as its bare
296/// payload, tried only after every tagged variant fails. On a variant serde
297/// skips both ways it is in no schema, and is accepted.
298/// - `#[serde(flatten)]` onto a field whose schema names none of its members,
299/// which a map's does not. Its `additionalProperties` is defined against the
300/// `properties` of its own schema object, and composing it into the parent's
301/// `allOf` leaves it none — so the map's *value* schema would apply to the
302/// properties the parent declared itself. Refused by a
303/// [`Flatten`](https://docs.rs/kynos/latest/kynos/schema/flatten/trait.Flatten.html)
304/// bound the expansion asserts per flattened field. `#[schema(open)]` on that
305/// field is the opt-in that keeps the map: it says the object really is open,
306/// and the map's values become the parent's `unevaluatedProperties`, the one
307/// keyword that sees annotations across an `allOf`. The same bound holds the
308/// payload of an internally tagged enum's newtype variant, which is composed
309/// beside the tag in an `allOf` the same way.
310/// - A `#[serde(other)]` catch-all on a variant serde reads, which only 3.2's
311/// `defaultMapping` could describe and the derive does not emit. On a variant
312/// serde skips both ways it catches nothing, and is accepted.
313/// - `skip_serializing_if` on a non-`Option` field with no `#[serde(default)]`
314/// on the field or its struct, and `#[serde(skip_serializing)]` alone on such
315/// a field. serde may leave the field out of what it writes but still
316/// requires it on read, so no `required` list is true in both directions.
317/// Add `#[serde(default)]` beside it or on the struct. Exempt are a field
318/// serde never reads and a flattened `PhantomData`, both in no schema, any
319/// field of a variant serde never writes, which serde only reads, and any
320/// field of a `transparent` struct, which has no `required` list. Any other
321/// flattened field is decided by `#[schema(open)]` alone, since serde
322/// ignores any default on it: an open map is exempt, and anything else is
323/// refused.
324/// - `#[serde(into = ...)]`, `from` or `try_from` on the type itself, struct or
325/// enum. serde then writes or reads the type they name, which the declared
326/// fields or variants no longer predict. Implement `Schema` by hand, describing
327/// the type serde converts through. `#[serde(remote = ...)]` is accepted,
328/// since its fields mirror the type it names.
329/// - `#[serde(transparent)]` on a struct serde writes through one field and
330/// reads through another. serde writes through the field without `skip` or
331/// `skip_serializing` and reads through the field without `skip`,
332/// `skip_deserializing` or a field-level `default`; where each direction picks
333/// a single field, the two must be the same. A struct where only one direction
334/// picks a single field is described by it, since serde refuses the other
335/// derive itself. Mark every other field `#[serde(skip)]`.
336/// - `#[serde(skip_serializing)]` or `skip_deserializing` alone on a member of a
337/// tuple struct, a tuple variant or a newtype variant, or `skip_serializing_if`
338/// on a tuple member other than the last one under a `#[serde(default)]` on it
339/// or its struct. A position, or a variant's payload, is there or not as a
340/// whole, so serde would write one shape and read another. `#[serde(skip)]`
341/// leaves the member out both ways; a newtype struct is exempt, since serde
342/// ignores all three there, and so are a newtype variant's
343/// `skip_serializing_if` and a `transparent` struct, which is not an array.
344/// Inside a variant serde never writes, `skip_serializing` on the variant,
345/// serde only reads the members, so there only a lone `skip_deserializing`
346/// is refused; a variant serde skips both ways is in no schema.
347/// - `#[serde(skip)]` on the only member of an adjacently tagged newtype variant,
348/// unless that member is an `Option`. serde writes the variant as its tag
349/// alone but reads it only with its content, so no one schema is true of
350/// both. Make the member an `Option`, or skip the whole variant.
351/// - `#[serde(skip_deserializing)]` alone on a variant. serde writes the variant
352/// and refuses to read it back, so a closed `oneOf` or `enum` listing it
353/// describes a request serde refuses, and one leaving it out describes a
354/// response serde writes. `#[serde(skip)]` leaves the variant out both ways.
355/// `skip_serializing` alone on a variant is accepted, since every variant serde
356/// writes is one it reads.
357/// - `#[serde(skip_deserializing)]` without `skip_serializing` on a named field
358/// of an object that `deny_unknown_fields` closes, or beside a
359/// `#[schema(open)]` flattened field whose type does not implement
360/// [`AdmitsAny`](https://docs.rs/kynos/latest/kynos/schema/flatten/trait.AdmitsAny.html),
361/// which a map, whose value schema is hoisted, does not unless its values are
362/// `Unchecked`, and an `Unchecked` map does. serde writes the field and never
363/// reads it, so the schema leaves it out, and the object then refuses what
364/// serde writes of it: through being closed, or through the value schema a
365/// map hoists as `unevaluatedProperties`. `#[serde(skip)]` leaves the field
366/// out both ways. For the same reason a
367/// struct, or an internally tagged enum whose struct variant serde writes,
368/// holding such a field does not implement `Flatten`.
369/// - A flattened `#[schema(open)]` field beside `#[serde(deny_unknown_fields)]`.
370/// serde refuses every key the object's fields do not name before the map
371/// sees it, so it reads the map empty and writes members it would refuse to
372/// read back. Drop one of the two.
373/// - A flattened internally tagged enum, or a flattened struct holding a
374/// flattened field serde reads or carrying a container `#[serde(tag)]`, in
375/// an object `deny_unknown_fields` closes. serde takes a flattened key only
376/// for a type it reads by name, through `deserialize_struct`. It lends an
377/// internally tagged enum, and a struct holding a flattened field, every key
378/// without taking any, and never takes a struct's own tag, so the object
379/// refuses every document the type writes. Refused by a
380/// [`ClosedFlatten`](https://docs.rs/kynos/latest/kynos/schema/flatten/trait.ClosedFlatten.html)
381/// bound the expansion asserts per flattened field of a closed object beside
382/// `Flatten`, which the derive implements for a struct with no flattened
383/// field serde reads and no container tag, and for an adjacently tagged
384/// enum. Flatten one of those instead, or drop `deny_unknown_fields`.
385/// - A variant whose own name, after `rename` and `rename_all`, an earlier
386/// variant also reads, by its own name or an `alias`. serde reads a name as
387/// the first variant claiming it, so the later variant goes on the wire under
388/// a name that reads back as the earlier one. An `alias` an earlier variant
389/// already claims is accepted and named under that variant alone, since serde
390/// never reads it as the later one.
391/// - A container `tag` on a struct carrying `deny_unknown_fields`. serde writes
392/// the tag beside the fields and never reads it back as one of them, so the
393/// closed struct refuses every document it writes. Drop
394/// `deny_unknown_fields`, or drop the tag and declare it as a field. A
395/// `transparent` struct writes no tag, and is accepted.
396/// - A named field serde writes or reads under the name of its struct's own
397/// container `tag`, as its wire name or an `alias`. serde writes the key
398/// twice and reads the tag's value back as the field. Rename the field or the
399/// tag, or skip the field both ways; one serde skips in the direction it
400/// would collide in is accepted. A flattened field's own name is never
401/// written, so it is accepted too, but the keys its type writes are not
402/// checked: the derive cannot see them, as serde's own check of an enum's
403/// internal tag cannot, and one named as the tag gets a schema serde's
404/// document does not meet.
405#[proc_macro_derive(Schema, attributes(schema))]
406pub fn derive_schema(item: TokenStream) -> TokenStream {
407 derive::schema::expand(item)
408}
409
410/// Maps an error type to RFC 9457 problem details.
411///
412/// ```ignore
413/// #[derive(Debug, thiserror::Error, ApiError)]
414/// #[problem(base = "https://errors.example.com/")]
415/// enum StoreError {
416/// #[error("no user with id {id}")]
417/// #[problem(status = 404, title = "User not found")]
418/// NotFound {
419/// #[problem(extension)]
420/// id: UserId,
421/// trace: String,
422/// },
423///
424/// #[error("that email is already registered")]
425/// #[problem(status = 409)]
426/// EmailTaken,
427/// }
428/// ```
429///
430/// `status` is required on every variant and must be between 400 and 599. A
431/// struct declares its one status on the type instead; `base` always belongs on
432/// the type, since it is the prefix every variant's type URI shares.
433///
434/// The error's `Display` supplies each problem's `detail`, which is why
435/// `thiserror` is the expected companion — the `#[error("...")]` a Rust reader
436/// sees is the sentence an API consumer receives. A type without a `Display`
437/// is rejected at the derive rather than at the handler returning it.
438///
439/// A field is published as an extension member only when it says
440/// `#[problem(extension)]`, because a variant carries whatever the error site
441/// had to hand and the default must not be to put that on the wire.
442///
443/// Also emits the `IntoResponse` and `Responses` implementations, so the
444/// statuses the error can produce and the statuses the description advertises
445/// cannot diverge. It is the only supported way to implement
446/// `IntoProblem`.
447#[proc_macro_derive(ApiError, attributes(problem))]
448pub fn derive_api_error(item: TokenStream) -> TokenStream {
449 derive::api_error::expand(item)
450}
451
452/// Declares a closed set of responses, one variant per status.
453///
454/// ```ignore
455/// #[derive(Reply)]
456/// enum CreateReply {
457/// #[reply(status = 201, description = "the user as stored")]
458/// Created(User),
459///
460/// #[reply(status = 200, description = "an identical user already existed")]
461/// AlreadyExists(User),
462/// }
463/// ```
464///
465/// For an operation with more than one success shape. Modelled on
466/// poem-openapi's `ApiResponse`, which is the best existing treatment of this.
467///
468/// `status` is required on every variant and must be between 200 and 599: a 1xx
469/// is an interim response, and a handler returns the final one. No two variants
470/// may declare the same status, since the description keys a reply's variants
471/// by status alone — that is what "one variant per status" means, and it is the
472/// one place this derive is stricter than [`ApiError`](macro@ApiError), whose
473/// variants carry a `detail` that tells two occurrences of a status apart.
474///
475/// A variant's fields are its response body, so a variant holds either nothing,
476/// for the empty body, or exactly one type describing the body. An anonymous
477/// record has no name to register a component under.
478#[proc_macro_derive(Reply, attributes(reply))]
479pub fn derive_reply(item: TokenStream) -> TokenStream {
480 derive::reply::expand(item)
481}
482
483/// Declares a group of path parameters.
484///
485/// Wire names must match the route template's variables in declaration order;
486/// the route attribute emits a const assertion comparing the two lists. A
487/// field's wire name is its `#[param(rename)]`, else serde's `rename`, else
488/// its identifier under the struct's `rename_all`, cased as the
489/// [`Schema`](macro@Schema) derive cases a property, so a field no Kynos
490/// attribute renames carries the name that derive would give its property.
491/// Each field's type, or an `Option`'s inner type, is a
492/// `kynos::schema::ParamValue`.
493///
494/// # Rejected, because a parameter has one name
495///
496/// - `#[serde(alias = "...")]` on any field: serde would read the field under
497/// a second name, and a Parameter Object carries one.
498/// - serde's split `rename(serialize = ..., deserialize = ...)` on a field the
499/// Kynos attribute does not name, and `rename_all(serialize = ...,
500/// deserialize = ...)` on the struct.
501#[proc_macro_derive(PathParams, attributes(param))]
502pub fn derive_path_params(item: TokenStream) -> TokenStream {
503 derive::path_params::expand(item)
504}
505
506/// Declares a group of query parameters.
507///
508/// Each field is one parameter, decoded from its value through `FromStr`, so
509/// its type, or an `Option`'s inner type, must be a
510/// `kynos::schema::ParamValue`: a field whose schema is an object is refused,
511/// since the default `form` style with `explode` would describe it as `x=1&y=2`
512/// while the decoder reads one `name=` pair. For a structured query,
513/// `QueryString<T, M>` describes the whole query string under `openapi32`.
514///
515/// A field's wire name is its `#[param(rename)]`, else serde's `rename`, else
516/// its identifier under the struct's `rename_all`, cased as the
517/// [`Schema`](macro@Schema) derive cases a property, so a field no Kynos
518/// attribute renames carries the name the `Schema` derive gives its property.
519/// The `Schema` derive never reads `#[param(rename)]`, so a field that
520/// attribute renames is a parameter under one name and a property under
521/// another.
522///
523/// # Rejected, because a parameter has one name
524///
525/// - `#[serde(alias = "...")]` on any field: serde would read the field under
526/// a second name, and a Parameter Object carries one.
527/// - serde's split `rename(serialize = ..., deserialize = ...)` on a field the
528/// Kynos attribute does not name, and `rename_all(serialize = ...,
529/// deserialize = ...)` on the struct.
530#[proc_macro_derive(QueryParams, attributes(param))]
531pub fn derive_query_params(item: TokenStream) -> TokenStream {
532 derive::query_params::expand(item)
533}
534
535/// Declares a group of request or response headers.
536///
537/// Rejects `Accept`, `Content-Type` and `Authorization`. The specification says
538/// a parameter definition for those is ignored; `Content-Type` is likewise
539/// derived from a response's content map. Repeated fields such as `Set-Cookie`
540/// remain separate header values rather than being comma joined. The
541/// diagnostic names the right tool for each reserved field. Each field's type,
542/// or an `Option`'s inner type, is a `kynos::schema::ParamValue`, in either
543/// direction.
544///
545/// A field's wire name is its `#[header(rename)]`, else serde's `rename`, else
546/// its identifier under the struct's `rename_all`, cased as the
547/// [`Schema`](macro@Schema) derive cases a property, so `rename_all =
548/// "kebab-case"` names `x_request_id` as `x-request-id`. The reserved names
549/// are checked against that final name.
550///
551/// # Rejected, because a parameter has one name
552///
553/// - `#[serde(alias = "...")]` on any field: serde would read the field under
554/// a second name, and a Parameter Object carries one.
555/// - serde's split `rename(serialize = ..., deserialize = ...)` on a field the
556/// Kynos attribute does not name, and `rename_all(serialize = ...,
557/// deserialize = ...)` on the struct.
558#[proc_macro_derive(HeaderParams, attributes(header))]
559pub fn derive_headers(item: TokenStream) -> TokenStream {
560 derive::headers::expand(item)
561}
562
563/// Declares a group of request cookies.
564///
565/// Each field's type, or an `Option`'s inner type, is a
566/// `kynos::schema::ParamValue`. A field's wire name is its
567/// `#[cookie(rename)]`, else serde's `rename`, else its identifier under the
568/// struct's `rename_all`, cased as the [`Schema`](macro@Schema) derive cases a
569/// property.
570///
571/// # Rejected, because a parameter has one name
572///
573/// - `#[serde(alias = "...")]` on any field: serde would read the field under
574/// a second name, and a Parameter Object carries one.
575/// - serde's split `rename(serialize = ..., deserialize = ...)` on a field the
576/// Kynos attribute does not name, and `rename_all(serialize = ...,
577/// deserialize = ...)` on the struct.
578#[proc_macro_derive(CookieParams, attributes(cookie))]
579pub fn derive_cookies(item: TokenStream) -> TokenStream {
580 derive::cookies::expand(item)
581}
582
583/// Declares the fields of a `multipart/form-data` body, in both directions.
584///
585/// ```ignore
586/// #[derive(Schema, MultipartForm)]
587/// struct Upload {
588/// name: String,
589/// caption: Option<String>,
590/// images: Vec<FilePart>,
591/// }
592/// ```
593///
594/// One declaration, two implementations: `FromMultipart` reads each field from
595/// the part carrying its name, and `IntoMultipart` writes it back under the
596/// same one — so a body `MultipartForm<T>` accepts is a body it can produce.
597///
598/// A field's type says how many parts carry it: `Vec<T>` is one part per
599/// element, `Option<T>` is a part that need not have been sent, and anything
600/// else is a part that must have been, whose second and later occurrences are
601/// ignored. The element type converts through `FromPart` and `IntoPart`, which
602/// Kynos implements for `FilePart`, `String` and `Bytes`.
603///
604/// A part naming no declared field is ignored, since a form may carry what the
605/// agent rendering it added.
606///
607/// There is no attribute of its own. Derive [`Schema`](macro@Schema) alongside:
608/// this derive says how the parts travel and `Schema` is what puts them in the
609/// description, and both read the part names from the same place — the field's
610/// identifier, or serde's `rename` and `rename_all` when the type carries them.
611#[proc_macro_derive(MultipartForm)]
612pub fn derive_multipart_form(item: TokenStream) -> TokenStream {
613 derive::multipart::expand(item)
614}
615
616/// Declares a tag.
617///
618/// ```ignore
619/// #[derive(Tag)]
620/// #[tag(name = "users", description = "Managing user accounts")]
621/// struct Users;
622/// ```
623///
624/// Tags are types rather than strings, so a typo is a compile error and
625/// uniqueness follows from the module system. `#[tag(parent = Admin)]` nests
626/// one tag under another, which requires `openapi32`.
627#[proc_macro_derive(Tag, attributes(tag))]
628pub fn derive_tag(item: TokenStream) -> TokenStream {
629 derive::tag::expand(item)
630}
631
632/// Declares a security scheme.
633///
634/// ```ignore
635/// #[derive(SecurityScheme)]
636/// #[security(http, scheme = "bearer", bearer_format = "JWT")]
637/// struct Bearer;
638/// ```
639#[proc_macro_derive(SecurityScheme, attributes(security))]
640pub fn derive_security_scheme(item: TokenStream) -> TokenStream {
641 derive::security_scheme::expand(item)
642}
643
644/// Declares an application context, emitting one `Provides` implementation per
645/// field.
646///
647/// ```ignore
648/// #[derive(Provider)]
649/// struct App {
650/// pool: Pool,
651/// cache: Cache,
652/// #[provide(skip)]
653/// started_at: Instant,
654/// }
655/// ```
656///
657/// Each provided field's type must be `Clone`, since a value is handed out per
658/// request; a handle is the intended shape. A handler asking for something no
659/// field supplies fails to typecheck, rather than panicking at runtime the way
660/// an erased state map does.
661///
662/// Two provided fields of the same type are rejected here, naming both, rather
663/// than being left to produce a coherence error about the derive's own output.
664#[proc_macro_derive(Provider, attributes(provide))]
665pub fn derive_provider(item: TokenStream) -> TokenStream {
666 derive::provider::expand(item)
667}