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