oapi_codegen/ir.rs
1//! Intermediate representation (IR) of generated Rust types.
2//!
3//! The schema mapping pass ([`crate::lower::schema`]) lowers OpenAPI schemas into this
4//! IR. The emit pass ([`crate::emit`]) turns the IR into a token stream. Keeping
5//! the two separate makes the mapping logic testable without touching token
6//! generation, and keeps emission free of OpenAPI concerns.
7
8use crate::naming::RustIdent;
9
10/// A generated Rust source module: an ordered list of top-level items.
11#[derive(Debug, Default, Clone, PartialEq)]
12pub struct Module {
13 /// Top-level type declarations, in deterministic emission order.
14 pub items: Vec<Item>,
15}
16
17/// A top-level item declaration.
18#[derive(Debug, Clone, PartialEq)]
19#[non_exhaustive]
20pub enum Item {
21 /// A `struct` declaration.
22 Struct(Struct),
23 /// An `enum` declaration (string enum or union).
24 Enum(Enum),
25 /// A `type X = Y;` alias.
26 Alias(Alias),
27}
28
29impl Item {
30 /// The declared name of the item, used for stable ordering.
31 pub fn name(&self) -> &str {
32 let name = match self {
33 Item::Struct(s) => s.name.logical(),
34 Item::Enum(e) => e.name.logical(),
35 Item::Alias(a) => a.name.logical(),
36 };
37 return name;
38 }
39}
40
41/// A generated `struct`.
42#[derive(Debug, Clone, PartialEq)]
43pub struct Struct {
44 /// Type name.
45 pub name: RustIdent,
46 /// Doc comment derived from the schema `description`.
47 pub doc: Option<String>,
48 /// `#[deprecated]` annotation from `deprecated: true` (+ `x-deprecated-reason`).
49 pub deprecated: Option<Deprecation>,
50 /// Named fields, in deterministic order.
51 pub fields: Vec<Field>,
52 /// When set, the struct captures unknown keys into a flattened map of this
53 /// element type (`additionalProperties`).
54 pub additional_properties: Option<RustType>,
55 /// Whether the schema set `additionalProperties: false`, which becomes
56 /// `#[serde(deny_unknown_fields)]`.
57 ///
58 /// This is a field of its own and not the `None` case of
59 /// [`Self::additional_properties`], because an absent `additionalProperties`
60 /// and an explicit `false` are different statements. An absent key permits
61 /// unknown keys and drops them, which is what serde does with no attribute.
62 /// An explicit `false` rejects them. Both give no flattened map, so the map
63 /// alone cannot tell them apart.
64 pub deny_unknown_fields: bool,
65}
66
67/// A single struct field.
68#[derive(Debug, Clone, PartialEq)]
69pub struct Field {
70 /// Rust field identifier.
71 pub name: RustIdent,
72 /// `#[serde(rename = "...")]` value, when the wire name differs.
73 pub rename: Option<String>,
74 /// Doc comment derived from the property `description`.
75 pub doc: Option<String>,
76 /// `#[deprecated]` annotation from `deprecated: true` (+ `x-deprecated-reason`).
77 pub deprecated: Option<Deprecation>,
78 /// Field type (already wrapped in `Option<..>` when optional).
79 pub ty: RustType,
80 /// Whether the property is required. Optional fields get
81 /// `#[serde(skip_serializing_if = "Option::is_none")]` unless [`Self::omit_empty`]
82 /// overrides it.
83 pub required: bool,
84 /// `x-omitempty` override: `Some(true)`/`Some(false)` forces the
85 /// `skip_serializing_if` on/off. `None` keeps the default (skip when optional).
86 pub omit_empty: Option<bool>,
87 /// `x-rust-serde-skip`: drop the field from (de)serialization via `#[serde(skip)]`.
88 pub serde_skip: bool,
89 /// The value that serde uses when the property is absent, from `default`.
90 ///
91 /// An optional property with a default is *not* wrapped in `Option`. Once
92 /// parsed, it always holds a value. Only `nullable` keeps the `Option`,
93 /// because there `null` is a value that the property can carry.
94 ///
95 /// `default: null` never reaches here. The parser reads it as no default at
96 /// all, and serde already leaves a missing `Option` as `None`.
97 pub default: Option<DefaultValue>,
98 /// The validation keywords the property declares, when it declares any.
99 ///
100 /// The generator checks these on the way in, so the check runs only where
101 /// the code deserializes. A response the server writes is not checked.
102 pub constraints: Option<Constraints>,
103 /// Which direction of an exchange carries the property, from `readOnly` and
104 /// `writeOnly`.
105 pub access: Access,
106}
107
108/// One direction of an exchange.
109///
110/// A request travels from the client to the server. A response travels back.
111/// The direction does not name a serde trait, because a server deserializes a
112/// request and serializes a response, and a client does the opposite.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114#[non_exhaustive]
115pub enum Direction {
116 /// The payload an operation reads: parameters and the request body.
117 Request,
118 /// The payload an operation writes: the response body and its headers.
119 Response,
120}
121
122/// Which direction of an exchange carries a property.
123///
124/// `readOnly` gives [`Access::ReadOnly`] and `writeOnly` gives
125/// [`Access::WriteOnly`]. [`crate::lower::direction`] then drops a property that
126/// the direction of a shape does not carry.
127#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
128#[non_exhaustive]
129pub enum Access {
130 /// Both directions carry the property.
131 #[default]
132 ReadWrite,
133 /// A response carries the property, and a request must not.
134 ReadOnly,
135 /// A request carries the property, and a response must not.
136 WriteOnly,
137}
138
139impl Access {
140 /// Whether `direction` carries a property with this access.
141 pub fn carried_by(self, direction: Direction) -> bool {
142 return match (self, direction) {
143 (Access::ReadWrite, _) => true,
144 (Access::ReadOnly, Direction::Response) | (Access::WriteOnly, Direction::Request) => true,
145 (Access::ReadOnly, Direction::Request) | (Access::WriteOnly, Direction::Response) => false,
146 };
147 }
148}
149
150/// A numeric bound, in the form the document writes it.
151///
152/// A bound holds one number, so it copies. `f64` has no `Eq`, so the list stops
153/// at `PartialEq`.
154#[derive(Debug, Clone, Copy, PartialEq)]
155#[non_exhaustive]
156pub enum Bound {
157 /// A bound on an `integer` schema.
158 Int(i64),
159 /// A bound on a `number` schema.
160 Float(f64),
161}
162
163/// The validation keywords a schema declares.
164///
165/// A field with no keyword carries `None`, so the common schema costs nothing.
166#[derive(Debug, Clone, PartialEq, Default)]
167pub struct Constraints {
168 /// `pattern`: the value must match this regular expression.
169 pub pattern: Option<String>,
170 /// `minLength`, counted in characters, as JSON Schema counts them.
171 pub min_length: Option<usize>,
172 /// `maxLength`, counted in characters.
173 pub max_length: Option<usize>,
174 /// `minimum`.
175 pub minimum: Option<Bound>,
176 /// `maximum`.
177 pub maximum: Option<Bound>,
178 /// `exclusiveMinimum`: makes `minimum` a strict bound.
179 pub exclusive_minimum: bool,
180 /// `exclusiveMaximum`: makes `maximum` a strict bound.
181 pub exclusive_maximum: bool,
182 /// Whether `minimum` moved to absorb an `exclusiveMinimum`.
183 ///
184 /// The bound then holds a number the document does not write, and a message
185 /// about it must name the number the author wrote instead.
186 pub folded_minimum: bool,
187 /// Whether `maximum` moved to absorb an `exclusiveMaximum`.
188 pub folded_maximum: bool,
189 /// `multipleOf`.
190 pub multiple_of: Option<Bound>,
191 /// `minItems`.
192 pub min_items: Option<usize>,
193 /// `maxItems`.
194 pub max_items: Option<usize>,
195 /// `uniqueItems`.
196 pub unique_items: bool,
197 /// `minProperties`.
198 pub min_properties: Option<usize>,
199 /// `maxProperties`.
200 pub max_properties: Option<usize>,
201 /// The type the checks run against, when the field names an alias.
202 ///
203 /// A `$ref` to a constrained scalar gives the field a named type, and the
204 /// name alone does not say which check applies. Lowering resolves the ref
205 /// and records the type behind the name here.
206 pub checked_as: Option<RustType>,
207}
208
209impl Constraints {
210 /// Whether the schema declares no keyword at all.
211 pub fn is_empty(&self) -> bool {
212 return *self == Self::default();
213 }
214}
215
216/// The value that serde uses when a property is absent.
217///
218/// Lowered from the schema `default` and already checked against the field type.
219/// The JSON value alone is not enough to write Rust. `1` is `1` for an integer
220/// field and `1.0` for a number field. `"active"` is a string for a `String`
221/// field and a variant path for an enum.
222#[derive(Debug, Clone, PartialEq)]
223#[non_exhaustive]
224pub enum DefaultValue {
225 /// A string literal.
226 Str(String),
227 /// An integer literal.
228 Int(i64),
229 /// An unsigned integer literal.
230 ///
231 /// An unsigned field takes this rather than [`Self::Int`], which keeps a
232 /// negative value out and reaches the whole range of `u64`, above where
233 /// `i64` stops.
234 UInt(u64),
235 /// A floating-point literal.
236 Float(f64),
237 /// A `bool` literal.
238 Bool(bool),
239 /// A unit variant of a generated string enum, named by its identifier.
240 Variant(RustIdent),
241 /// An empty collection, which `Default::default()` gives for both `Vec` and
242 /// `HashMap`.
243 Empty,
244}
245
246/// A generated `enum`.
247#[derive(Debug, Clone, PartialEq)]
248pub struct Enum {
249 /// Type name.
250 pub name: RustIdent,
251 /// Doc comment derived from the schema `description`.
252 pub doc: Option<String>,
253 /// `#[deprecated]` annotation from `deprecated: true` (+ `x-deprecated-reason`).
254 pub deprecated: Option<Deprecation>,
255 /// The flavour of enum to emit.
256 pub kind: EnumKind,
257}
258
259/// The flavour of a generated enum.
260#[derive(Debug, Clone, PartialEq)]
261#[non_exhaustive]
262pub enum EnumKind {
263 /// A C-like string enum: unit variants mapped to wire strings.
264 Strings(Vec<StringVariant>),
265 /// A C-like integer enum: unit variants with explicit discriminants.
266 ///
267 /// The wire form is a bare number, so the emitted type carries
268 /// `#[serde(try_from, into)]` and a `#[repr]` of [`Self::Integers::repr`].
269 Integers {
270 /// The integer type the discriminants take: `i32`, `i64`, `u32`, or `u64`.
271 repr: RustType,
272 /// The permitted values, in document order.
273 variants: Vec<IntegerVariant>,
274 },
275 /// A `#[serde(untagged)]` union over the given newtype variants.
276 ///
277 /// Untagged (rather than internally tagged) is used even when the OpenAPI
278 /// schema has a discriminator: OpenAPI variant schemas typically carry the
279 /// discriminator property themselves, which is incompatible with serde's
280 /// internally-tagged representation.
281 Union(Vec<UnionVariant>),
282}
283
284/// A unit variant of a string enum.
285#[derive(Debug, Clone, PartialEq)]
286pub struct StringVariant {
287 /// Rust variant identifier.
288 pub name: RustIdent,
289 /// `#[serde(rename = "...")]` value, when the wire value differs.
290 pub rename: Option<String>,
291 /// Doc comment, if any.
292 pub doc: Option<String>,
293}
294
295/// A unit variant of an integer enum.
296#[derive(Debug, Clone, PartialEq)]
297pub struct IntegerVariant {
298 /// Rust variant identifier.
299 pub name: RustIdent,
300 /// The discriminant, and the value on the wire.
301 pub value: i64,
302 /// Doc comment, if any.
303 pub doc: Option<String>,
304}
305
306/// A newtype variant of a union enum.
307#[derive(Debug, Clone, PartialEq)]
308pub struct UnionVariant {
309 /// Rust variant identifier.
310 pub name: RustIdent,
311 /// The wrapped type.
312 pub ty: RustType,
313}
314
315/// A generated `type X = Y;` alias.
316#[derive(Debug, Clone, PartialEq)]
317pub struct Alias {
318 /// Alias name.
319 pub name: RustIdent,
320 /// Doc comment, if any.
321 pub doc: Option<String>,
322 /// `#[deprecated]` annotation from `deprecated: true` (+ `x-deprecated-reason`).
323 pub deprecated: Option<Deprecation>,
324 /// Aliased type.
325 pub ty: RustType,
326}
327
328/// A `#[deprecated]` annotation derived from `deprecated: true`, optionally
329/// carrying the `x-deprecated-reason` text as the `note`.
330#[derive(Debug, Clone, PartialEq)]
331pub struct Deprecation {
332 /// The `x-deprecated-reason` text, emitted as `#[deprecated(note = "...")]`.
333 pub note: Option<String>,
334}
335
336/// Which of the three non-serde traits every generated model derives a foreign
337/// type satisfies.
338///
339/// The generator derives `Debug`, `Clone`, and `PartialEq` on every model. A
340/// model that holds a type the generator did not write cannot derive a trait that
341/// type lacks. The generator also cannot inspect the type, because the
342/// specification names it as text and `rustc` resolves it much later. So the
343/// specification author declares it, with `x-rust-derive`.
344///
345/// [`Self::default`] claims all three. That keeps the output of every
346/// specification written before this feature identical, and it is right far more
347/// often than not. `uuid::Uuid`, the `chrono` types, and a typical hand-written
348/// domain type all derive the three. A target that does not is the case worth one
349/// line of specification.
350#[derive(Debug, Clone, Copy, PartialEq, Eq)]
351pub struct ForeignDerives {
352 /// The target implements [`std::fmt::Debug`].
353 pub debug: bool,
354 /// The target implements [`Clone`].
355 pub clone: bool,
356 /// The target implements [`PartialEq`].
357 pub partial_eq: bool,
358}
359
360impl Default for ForeignDerives {
361 fn default() -> Self {
362 return Self {
363 debug: true,
364 clone: true,
365 partial_eq: true,
366 };
367 }
368}
369
370impl ForeignDerives {
371 /// Whether this target satisfies all three traits, and therefore constrains
372 /// nothing. The common case, and the one that needs no record anywhere.
373 pub(crate) fn is_unconstrained(self) -> bool {
374 return self.debug && self.clone && self.partial_eq;
375 }
376
377 /// Narrow to the traits both `self` and `other` satisfy.
378 ///
379 /// A model holding two foreign types can derive only what both allow, so the
380 /// walk over a model's referenced types folds with this.
381 pub(crate) fn intersect(self, other: Self) -> Self {
382 return Self {
383 debug: self.debug && other.debug,
384 clone: self.clone && other.clone,
385 partial_eq: self.partial_eq && other.partial_eq,
386 };
387 }
388}
389
390/// A Rust type expression usable in field/alias position.
391#[derive(Debug, Clone, PartialEq, Eq)]
392#[non_exhaustive]
393pub enum RustType {
394 /// `bool`.
395 Bool,
396 /// `i32`.
397 I32,
398 /// `i64`.
399 I64,
400 /// `u32`.
401 U32,
402 /// `u64`.
403 U64,
404 /// `f64`.
405 F64,
406 /// `String`.
407 String,
408 /// `serde_json::Value` (free-form / empty schema).
409 Value,
410 /// `chrono::NaiveDate` (`format: date`).
411 Date,
412 /// `chrono::DateTime<chrono::Utc>` (`format: date-time`).
413 DateTime,
414 /// `uuid::Uuid` (`format: uuid`).
415 Uuid,
416 /// `Vec<u8>` (`format: byte`/`binary`).
417 Bytes,
418 /// `Vec<T>`.
419 Vec(Box<RustType>),
420 /// `std::collections::HashMap<String, T>`.
421 Map(Box<RustType>),
422 /// `Option<T>`.
423 Option(Box<RustType>),
424 /// `Box<T>`, added by the recursion pass to give a cyclic type a size.
425 ///
426 /// Nothing in a schema asks for this. `lower::recurse` inserts it where a
427 /// type would otherwise hold itself, directly or through other types.
428 Boxed(Box<RustType>),
429 /// A reference to a named (generated or external) type.
430 Named(String),
431 /// A reference to a type from an import-mapped module: `module::Name`.
432 ///
433 /// Generated by another run of this generator, so it derives all three
434 /// non-serde traits and carries no [`ForeignDerives`].
435 External { module: String, name: String },
436 /// A verbatim type expression from an `x-rust-type` extension, with what its
437 /// `x-rust-derive` says the target implements.
438 Verbatim {
439 /// The Rust type expression, emitted as written.
440 text: String,
441 /// Which of `Debug`, `Clone`, `PartialEq` the target satisfies.
442 derives: ForeignDerives,
443 },
444}
445
446impl RustType {
447 /// Wrap this type in `Option<..>`.
448 pub fn optional(self) -> RustType {
449 return RustType::Option(Box::new(self));
450 }
451
452 /// Whether this type is an `Option<..>` (for which `skip_serializing_if =
453 /// "Option::is_none"` is valid).
454 pub fn is_option(&self) -> bool {
455 return matches!(self, RustType::Option(_));
456 }
457
458 /// The type inside any `Option` or `Box` wrapper.
459 ///
460 /// A `Vec` and a `Map` are not wrappers here. Each carries keywords of its
461 /// own, so a rule reads the collection and not an element.
462 pub fn innermost(&self) -> &RustType {
463 return match self {
464 RustType::Option(inner) | RustType::Boxed(inner) => inner.innermost(),
465 other => other,
466 };
467 }
468
469 /// Whether this is a scalar the generated code compares with `==`.
470 pub fn is_scalar(&self) -> bool {
471 return matches!(
472 self,
473 RustType::Bool
474 | RustType::I32
475 | RustType::I64
476 | RustType::U32
477 | RustType::U64
478 | RustType::F64
479 | RustType::String
480 );
481 }
482
483 /// The type as text, for an error message.
484 ///
485 /// Emission goes through `emit::emit_type`, which builds tokens. This gives
486 /// a reader the same type in a message that the lowering stage can write,
487 /// where no token stream exists yet.
488 pub fn label(&self) -> String {
489 return match self {
490 RustType::Bool => "bool".to_owned(),
491 RustType::I32 => "i32".to_owned(),
492 RustType::I64 => "i64".to_owned(),
493 RustType::U32 => "u32".to_owned(),
494 RustType::U64 => "u64".to_owned(),
495 RustType::F64 => "f64".to_owned(),
496 RustType::String => "String".to_owned(),
497 RustType::Value => "serde_json::Value".to_owned(),
498 RustType::Date => "chrono::NaiveDate".to_owned(),
499 RustType::DateTime => "chrono::DateTime<chrono::Utc>".to_owned(),
500 RustType::Uuid => "uuid::Uuid".to_owned(),
501 RustType::Bytes => "Vec<u8>".to_owned(),
502 RustType::Vec(inner) => format!("Vec<{}>", inner.label()),
503 RustType::Map(inner) => format!("std::collections::HashMap<String, {}>", inner.label()),
504 RustType::Option(inner) => format!("Option<{}>", inner.label()),
505 RustType::Boxed(inner) => format!("Box<{}>", inner.label()),
506 RustType::Named(name) => name.clone(),
507 RustType::External { module, name } => format!("{module}::{name}"),
508 RustType::Verbatim { text, .. } => text.clone(),
509 };
510 }
511
512 /// An `x-rust-type` target whose `x-rust-derive` is absent, so it claims all
513 /// three non-serde traits. The common case, and the shape every test that
514 /// does not test this feature wants.
515 pub fn verbatim(text: impl Into<String>) -> RustType {
516 return RustType::Verbatim {
517 text: text.into(),
518 derives: ForeignDerives::default(),
519 };
520 }
521}
522
523/// A request or response body: its Rust type plus the wire content type that
524/// selects the axum extractor / response wrapper.
525#[derive(Debug, Clone, PartialEq)]
526pub struct Body {
527 /// The Rust type of the decoded body.
528 pub ty: RustType,
529 /// The content type that selects the extractor / response wrapper.
530 pub kind: BodyKind,
531}
532
533/// The supported request/response content type for a [`Body`].
534#[derive(Debug, Clone, Copy, PartialEq, Eq)]
535#[non_exhaustive]
536pub enum BodyKind {
537 /// `application/json` (and `+json` / charset variants) → `axum::Json`.
538 Json,
539 /// `text/plain` → `String`.
540 Text,
541 /// `application/x-www-form-urlencoded` → `axum::Form`.
542 Form,
543 /// `multipart/form-data` (request bodies only) → a hand-written
544 /// `FromRequest` extractor driving `axum::extract::Multipart`. Carried
545 /// alongside an [`Operation`]'s [`Multipart`], which holds the per-field
546 /// parsing detail the extractor needs.
547 Multipart,
548}
549
550/// A `multipart/form-data` request body lowered into a per-operation extractor.
551///
552/// axum has no *typed* multipart extractor, so the generator emits a dedicated
553/// struct (an operation artifact, like the query/header/cookie structs) plus a
554/// hand-written `FromRequest` implementation that drives
555/// `axum::extract::Multipart`, reads each declared field, and returns a
556/// `400 Bad Request` on a missing required field or an unparseable value.
557#[derive(Debug, Clone, PartialEq)]
558pub struct Multipart {
559 /// Struct name (`<Op>Multipart`), doubling as the generated `FromRequest`
560 /// extractor type and the `Api` method's `body` argument type.
561 pub name: RustIdent,
562 /// Fields parsed from the multipart stream, in declaration order.
563 pub fields: Vec<MultipartField>,
564}
565
566/// A single field of a [`Multipart`] body.
567#[derive(Debug, Clone, PartialEq)]
568pub struct MultipartField {
569 /// Wire field name (the part's `Content-Disposition` `name`).
570 pub wire_name: String,
571 /// Target struct field identifier on the generated `<Op>Multipart` struct.
572 pub rust_name: RustIdent,
573 /// The decoded field type: `Vec<u8>` for a binary (file) part, else a
574 /// scalar. This is the bare inner type even when the field is optional.
575 /// [`MultipartField::optional`] records whether the struct wraps it in
576 /// `Option<..>`.
577 pub ty: RustType,
578 /// Whether the generated struct wraps this field in `Option<..>` (true when
579 /// the property is not `required`, or is `nullable`). An absent
580 /// non-optional field is a `400`. An absent optional field is `None`.
581 pub optional: bool,
582 /// Whether the part is a binary/file field read as raw bytes (`Vec<u8>`).
583 pub is_file: bool,
584}
585
586/// An operation's request payload, when it declares one.
587///
588/// The three cases are mutually exclusive by construction, replacing what will
589/// otherwise be several mutually-exclusive `Option` fields on [`Operation`].
590#[derive(Debug, Clone, PartialEq)]
591#[non_exhaustive]
592pub enum RequestPayload {
593 /// A single supported content type (JSON, `text/plain`, or form), extracted
594 /// directly by the matching axum extractor.
595 Single(Body),
596 /// A `multipart/form-data` body, decoded by a generated [`Multipart`]
597 /// extractor.
598 Multipart(Multipart),
599 /// Several supported content types, dispatched at request time on the
600 /// incoming `Content-Type` header by a generated [`NegotiatedBody`]
601 /// `FromRequest` enum. An unrecognised or missing content type yields a
602 /// `415 Unsupported Media Type`.
603 Negotiated(NegotiatedBody),
604}
605
606/// A body offering several content-type representations, lowered into a
607/// generated enum with one variant per representation.
608///
609/// For a request the enum is a hand-written `FromRequest` that dispatches on
610/// `Content-Type`. for a response it is a plain enum the handler selects a
611/// representation from, which the generated `IntoResponse` renders with the
612/// matching `Content-Type`.
613#[derive(Debug, Clone, PartialEq)]
614pub struct NegotiatedBody {
615 /// Enum name — `<Op>RequestBody` for a request, `<Response><Variant>Body`
616 /// for a response — doubling as the argument/field type on the generated
617 /// interface.
618 pub name: RustIdent,
619 /// One variant per supported content type, in priority order (JSON > form >
620 /// text).
621 pub variants: Vec<BodyVariant>,
622}
623
624/// One content-type representation within a [`NegotiatedBody`].
625#[derive(Debug, Clone, PartialEq)]
626pub struct BodyVariant {
627 /// Variant identifier, named after the content kind (`Json`, `Form`,
628 /// `Text`).
629 pub variant: RustIdent,
630 /// The decoded body type and content kind for this representation.
631 pub body: Body,
632}
633
634/// The body of a response variant, when it declares supported content.
635#[derive(Debug, Clone, PartialEq)]
636#[non_exhaustive]
637pub enum ResponseBody {
638 /// A single supported content type, rendered by the matching axum response
639 /// wrapper.
640 Single(Body),
641 /// Several supported content types the handler chooses among. The generated
642 /// `IntoResponse` renders whichever representation the handler selected.
643 Negotiated(NegotiatedBody),
644}
645
646/// A generated axum server interface: the `Api` trait plus the operations that
647/// back its `Router`.
648#[derive(Debug, Default, Clone, PartialEq)]
649pub struct Service {
650 /// Operations in deterministic (document) order.
651 pub operations: Vec<Operation>,
652 /// Security schemes referenced by at least one operation, in the order they
653 /// are declared in `components.securitySchemes`. The client emitter turns
654 /// each into a credential field and a `with_<scheme>` builder setter. The
655 /// server emitter ignores them (server-side auth is not generated yet).
656 pub security_schemes: Vec<SecurityScheme>,
657}
658
659/// A security scheme the client can apply to outgoing requests.
660///
661/// Derived from a `components.securitySchemes` entry. only the schemes the
662/// client generator can carry as a stored credential are modelled here, plus an
663/// [`SecuritySchemeKind::Unsupported`] catch-all so an operation that *requires*
664/// an unmodelled scheme (for example OAuth2) can be rejected with a clear message.
665#[derive(Debug, Clone, PartialEq)]
666pub struct SecurityScheme {
667 /// The scheme's key in `components.securitySchemes`, matched against each
668 /// [`Operation::security`] entry.
669 pub key: String,
670 /// `snake_case` base name for the generated credential field and the
671 /// `with_<field>` builder setter.
672 pub field: RustIdent,
673 /// How the credential is carried on the request.
674 pub kind: SecuritySchemeKind,
675 /// Doc comment derived from the scheme's `description`.
676 pub doc: Option<String>,
677}
678
679/// How a [`SecurityScheme`]'s credential is applied to a request.
680#[derive(Debug, Clone, PartialEq)]
681#[non_exhaustive]
682pub enum SecuritySchemeKind {
683 /// `type: http, scheme: bearer` — `Authorization: Bearer <token>`.
684 HttpBearer,
685 /// `type: http, scheme: basic` — `Authorization: Basic <base64>`.
686 HttpBasic,
687 /// `type: apiKey, in: header` — the key is sent as the named header.
688 ApiKeyHeader(String),
689 /// `type: apiKey, in: query` — the key is sent as the named query parameter.
690 ApiKeyQuery(String),
691 /// `type: apiKey, in: cookie` — the key is sent as the named cookie.
692 ApiKeyCookie(String),
693 /// A scheme the client cannot carry as a stored credential (`oauth2`,
694 /// `openIdConnect`). The wrapped string is a human-readable reason used when
695 /// rejecting an operation that requires it.
696 Unsupported(String),
697}
698
699/// A single HTTP operation lowered for code generation.
700#[derive(Debug, Clone, PartialEq)]
701pub struct Operation {
702 /// `Api` trait method name (`snake_case`).
703 pub name: RustIdent,
704 /// Per-operation response enum name (`<Name>Response`).
705 pub response_enum: RustIdent,
706 /// Doc comment derived from the operation `summary`/`description`.
707 pub doc: Option<String>,
708 /// Lowercase HTTP method verb (`get`, `post`, …), which is also the
709 /// `axum::routing` helper name.
710 pub method: String,
711 /// Request path template, reused verbatim as the axum route (axum 0.8 and
712 /// OpenAPI share the `/{name}` path-parameter syntax).
713 pub path: String,
714 /// Typed path parameters, in path order.
715 pub path_params: Vec<Param>,
716 /// Generated query-parameter struct, when the operation declares query
717 /// parameters. Its name doubles as the `axum_extra::extract::Query<..>`
718 /// type and the `Api` method's `query` argument type.
719 pub query: Option<Struct>,
720 /// Generated header-parameter struct, when the operation declares header
721 /// parameters. Its name doubles as the generated `FromRequestParts`
722 /// extractor type and the `Api` method's `headers` argument type.
723 pub headers: Option<Headers>,
724 /// Generated cookie-parameter struct, when the operation declares cookie
725 /// parameters. Its name doubles as the generated `FromRequestParts`
726 /// extractor type and the `Api` method's `cookies` argument type.
727 pub cookies: Option<Cookies>,
728 /// Request payload (a single supported content type, a `multipart/form-data`
729 /// extractor, or a `Content-Type`-dispatched set of content types), when the
730 /// operation declares a request body.
731 pub request: Option<RequestPayload>,
732 /// Response variants, in declaration order.
733 pub responses: Vec<ResponseCase>,
734 /// Keys of the security schemes this operation applies, derived from its
735 /// effective security requirement (its own `security`, else the document's
736 /// global `security`). Each key matches a [`SecurityScheme::key`]. Empty
737 /// means the operation is unauthenticated.
738 pub security: Vec<String>,
739}
740
741/// A typed operation parameter (path parameter in the current slice).
742#[derive(Debug, Clone, PartialEq)]
743pub struct Param {
744 /// Rust argument identifier.
745 pub name: RustIdent,
746 /// Parameter type.
747 pub ty: RustType,
748}
749
750/// A generated per-operation header struct, extracted via a hand-written
751/// `axum::extract::FromRequestParts` implementation rather than serde, since
752/// header values are read and parsed individually from the request parts.
753#[derive(Debug, Clone, PartialEq)]
754pub struct Headers {
755 /// Struct name (`<Op>Headers`), doubling as the extractor type and the
756 /// `Api` method's `headers` argument type.
757 pub name: RustIdent,
758 /// Header fields, in declaration order.
759 pub params: Vec<HeaderParam>,
760}
761
762/// A single header parameter within a [`Headers`] struct.
763#[derive(Debug, Clone, PartialEq)]
764pub struct HeaderParam {
765 /// Rust field identifier (`snake_case`).
766 pub name: RustIdent,
767 /// The exact OpenAPI header name, used for the case-insensitive lookup in
768 /// the generated extractor (for example `X-Request-Id`).
769 pub header_name: String,
770 /// The parsed scalar type. Unlike [`Field`], this is the bare element type
771 /// even when the header is optional. The emitter adds the `Option<..>`
772 /// wrapper for absent headers.
773 pub ty: RustType,
774 /// Whether the header is required. A missing required header is a `400`.
775 pub required: bool,
776 /// Doc comment derived from the parameter `description`.
777 pub doc: Option<String>,
778}
779
780/// A generated per-operation cookie struct, extracted via a hand-written
781/// `axum::extract::FromRequestParts` implementation backed by `axum_extra`'s
782/// `CookieJar`.
783#[derive(Debug, Clone, PartialEq)]
784pub struct Cookies {
785 /// Struct name (`<Op>Cookies`), doubling as the extractor type and the
786 /// `Api` method's `cookies` argument type.
787 pub name: RustIdent,
788 /// Cookie fields, in declaration order.
789 pub params: Vec<CookieParam>,
790}
791
792/// A single cookie parameter within a [`Cookies`] struct.
793#[derive(Debug, Clone, PartialEq)]
794pub struct CookieParam {
795 /// Rust field identifier (`snake_case`).
796 pub name: RustIdent,
797 /// The exact OpenAPI cookie name, used for the `CookieJar` lookup.
798 pub cookie_name: String,
799 /// The parsed scalar type. Like [`HeaderParam`], this is the bare element
800 /// type even when optional. The emitter adds the `Option<..>` wrapper.
801 pub ty: RustType,
802 /// Whether the cookie is required. A missing required cookie is a `400`.
803 pub required: bool,
804 /// Doc comment derived from the parameter `description`.
805 pub doc: Option<String>,
806}
807
808/// One arm of an operation's response enum.
809#[derive(Debug, Clone, PartialEq)]
810pub struct ResponseCase {
811 /// Variant identifier, named after the status reason phrase (fixed codes),
812 /// the response-range class, or `Default`.
813 pub variant: RustIdent,
814 /// How the variant's HTTP status code is determined.
815 pub status: ResponseStatus,
816 /// Response body (a single supported content type, or a set of content types
817 /// the handler chooses among), when the response declares supported content.
818 pub body: Option<ResponseBody>,
819 /// Declared response headers written by the generated `IntoResponse`, in
820 /// declaration order. Empty means no headers (the pre-C5 variant shape).
821 pub headers: Vec<ResponseHeader>,
822 /// Doc comment derived from the response `description`.
823 pub doc: Option<String>,
824}
825
826/// A single declared response header written by the generated `IntoResponse`.
827#[derive(Debug, Clone, PartialEq)]
828pub struct ResponseHeader {
829 /// Rust field identifier (`snake_case`).
830 pub name: RustIdent,
831 /// Exact header name as written to the response (for example `X-Request-Id`).
832 pub header_name: String,
833 /// Scalar type serialized to a header value via `ToString`. Bare element
834 /// type even when optional. The emitter adds the `Option<..>` wrapper.
835 pub ty: RustType,
836 /// Whether the header is always written (`false` → `Option<..>` field).
837 pub required: bool,
838 /// Doc comment derived from the header `description`.
839 pub doc: Option<String>,
840}
841
842/// How a response variant's HTTP status code is produced.
843///
844/// Fixed codes are emitted as a compile-time constant. `default` and range
845/// responses have no single code, so the variant instead carries an
846/// `axum::http::StatusCode` the handler supplies at runtime.
847#[derive(Debug, Clone, PartialEq, Eq)]
848#[non_exhaustive]
849pub enum ResponseStatus {
850 /// A concrete status code (for example `200`), emitted as a `StatusCode` constant.
851 Fixed(u16),
852 /// The `default` catch-all response. The handler supplies the status code.
853 Default,
854 /// A status-code range such as `5XX`, carrying the leading digit (`1..=5`).
855 /// the handler supplies a concrete code within the class.
856 Range(u8),
857}
858
859/// The lowered `servers:` block: constants and builder functions for each
860/// declared server URL, plus the enum types their variables reference.
861///
862/// Emitted when `generate.server-urls` is set. A server whose URL has no
863/// `{placeholder}` becomes a `const`. one with placeholders becomes a builder
864/// function that substitutes each variable and validates the result.
865#[derive(Debug, Clone, PartialEq)]
866pub struct ServerUrls {
867 /// Enum types for the enum-constrained server variables, emitted before the
868 /// builder functions that reference them.
869 pub enums: Vec<ServerUrlEnum>,
870 /// One entry per declared server, in document order.
871 pub servers: Vec<ServerUrl>,
872}
873
874/// A single lowered server URL: either a constant or a builder function.
875#[derive(Debug, Clone, PartialEq)]
876#[non_exhaustive]
877pub enum ServerUrl {
878 /// A server URL with no variables: `pub const <NAME>: &str = "<url>";`.
879 Const(ServerUrlConst),
880 /// A server URL with `{placeholder}`s: a builder function substituting them.
881 Builder(ServerUrlBuilder),
882}
883
884/// A variable-free server URL emitted as a string constant.
885#[derive(Debug, Clone, PartialEq)]
886pub struct ServerUrlConst {
887 /// `SCREAMING_SNAKE_CASE` constant name.
888 pub name: RustIdent,
889 /// Doc comment derived from the server `description`.
890 pub doc: Option<String>,
891 /// The literal server URL.
892 pub url: String,
893}
894
895/// A server URL with `{placeholder}`s emitted as a builder function that
896/// substitutes each variable and returns the resulting URL.
897#[derive(Debug, Clone, PartialEq)]
898pub struct ServerUrlBuilder {
899 /// `snake_case` function name.
900 pub name: RustIdent,
901 /// Doc comment derived from the server `description`.
902 pub doc: Option<String>,
903 /// The URL template, retaining its `{placeholder}` tokens.
904 pub url_template: String,
905 /// Parameters, sorted by placeholder name for a deterministic signature.
906 pub params: Vec<ServerUrlParam>,
907}
908
909/// One parameter of a [`ServerUrlBuilder`], bound to a URL placeholder.
910#[derive(Debug, Clone, PartialEq)]
911pub struct ServerUrlParam {
912 /// `snake_case` parameter identifier.
913 pub ident: RustIdent,
914 /// The placeholder name as it appears in the URL (without braces).
915 pub placeholder: String,
916 /// How the parameter is typed and turned into its substituted string.
917 pub ty: ServerUrlParamType,
918}
919
920/// The type of a [`ServerUrlParam`].
921#[derive(Debug, Clone, PartialEq)]
922#[non_exhaustive]
923pub enum ServerUrlParamType {
924 /// A free-form `&str` parameter (a non-enum or undeclared variable).
925 Str,
926 /// An enum-constrained parameter of the named [`ServerUrlEnum`] type.
927 Enum(RustIdent),
928}
929
930/// An enum type generated for an enum-constrained server variable.
931#[derive(Debug, Clone, PartialEq)]
932pub struct ServerUrlEnum {
933 /// `PascalCase` enum type name (`<Server><Variable>`).
934 pub name: RustIdent,
935 /// Doc comment naming the server variable this enum constrains.
936 pub doc: Option<String>,
937 /// The permitted values, in declaration order.
938 pub variants: Vec<ServerUrlEnumVariant>,
939 /// The variant identifier used for the `Default` impl (the OpenAPI
940 /// `default`), when the variable declares one.
941 pub default: Option<RustIdent>,
942}
943
944/// One variant of a [`ServerUrlEnum`]: a Rust identifier and its wire value.
945#[derive(Debug, Clone, PartialEq)]
946pub struct ServerUrlEnumVariant {
947 /// `PascalCase` variant identifier.
948 pub name: RustIdent,
949 /// The wire value substituted into the URL.
950 pub value: String,
951}