Skip to main content

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}