Skip to main content

forge_ir/
operation.rs

1//! Operations, parameters, request bodies, and responses.
2
3use serde::{Deserialize, Serialize};
4
5use crate::diagnostic::SpecLocation;
6use crate::security::SecurityRequirement;
7use crate::types::TypeRef;
8use crate::value::ValueRef;
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct Operation {
12    pub id: String,
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    pub original_id: Option<String>,
15    pub method: HttpMethod,
16    pub path_template: String,
17    #[serde(default, skip_serializing_if = "Vec::is_empty")]
18    pub path_params: Vec<Parameter>,
19    #[serde(default, skip_serializing_if = "Vec::is_empty")]
20    pub query_params: Vec<Parameter>,
21    #[serde(default, skip_serializing_if = "Vec::is_empty")]
22    pub header_params: Vec<Parameter>,
23    #[serde(default, skip_serializing_if = "Vec::is_empty")]
24    pub cookie_params: Vec<Parameter>,
25    /// OAS 3.2 `in: querystring` parameters — bind to the *entire*
26    /// query string (opaque pass-through). Spec semantics imply at most
27    /// one entry per operation; the parser warns rather than errors if
28    /// a spec declares multiple, and the IR carries them as a list for
29    /// uniformity with the other location buckets.
30    #[serde(default, skip_serializing_if = "Vec::is_empty")]
31    pub querystring_params: Vec<Parameter>,
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub request_body: Option<Body>,
34    #[serde(default, skip_serializing_if = "Vec::is_empty")]
35    pub responses: Vec<Response>,
36    #[serde(default, skip_serializing_if = "Vec::is_empty")]
37    pub security: Vec<SecurityRequirement>,
38    #[serde(default, skip_serializing_if = "Vec::is_empty")]
39    pub tags: Vec<String>,
40    /// OAS §4.10 `summary` — short one-line label. PathItem-level
41    /// `summary` fallback is applied at parse time.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub summary: Option<String>,
44    /// OAS §4.10 `description` — long-form CommonMark. PathItem-level
45    /// `description` fallback is applied at parse time.
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub description: Option<String>,
48    /// OAS §4.10 `deprecated`.
49    #[serde(default, skip_serializing_if = "crate::is_false")]
50    pub deprecated: bool,
51    /// Per-operation `externalDocs` (OAS §4.10).
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub external_docs: Option<crate::ExternalDocs>,
54    #[serde(default, skip_serializing_if = "Vec::is_empty")]
55    pub extensions: Vec<(String, ValueRef)>,
56    /// Effective `servers` list for this operation. The parser
57    /// resolves OAS §4.8.10 inheritance — operation-level entries win
58    /// over path-item entries, which win over the root list — and
59    /// materialises the result here. Empty if neither this operation,
60    /// its path item, nor the root declared any servers.
61    #[serde(default, skip_serializing_if = "Vec::is_empty")]
62    pub servers: Vec<crate::Server>,
63    /// OAS Callback Objects — out-of-band requests the API makes back
64    /// to the caller (event-driven / webhook APIs). Each entry pairs a
65    /// callback name with one runtime expression (e.g.
66    /// `{$request.body#/callbackUrl}`); a single callback name with
67    /// multiple expressions becomes multiple entries.
68    #[serde(default, skip_serializing_if = "Vec::is_empty")]
69    pub callbacks: Vec<crate::Callback>,
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub location: Option<SpecLocation>,
72}
73
74/// HTTP method for an operation. The eight standard verbs plus an
75/// `Other(String)` escape hatch that carries 3.2 `additionalOperations`
76/// methods (e.g. RFC 9205 `QUERY`) verbatim. Generators that emit
77/// against a fixed verb set should match the named variants and either
78/// reject `Other` (with a `StageError::Rejected`) or pass the string
79/// through to a lower-level builder API.
80#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
81#[serde(rename_all = "lowercase")]
82pub enum HttpMethod {
83    Get,
84    Put,
85    Post,
86    Delete,
87    Options,
88    Head,
89    Patch,
90    Trace,
91    /// Carries the upper-cased method name (`"QUERY"`, etc.).
92    Other(String),
93}
94
95impl HttpMethod {
96    /// Wire-form of the method (uppercased verb that goes on the
97    /// request line). Use this when serialising to HTTP.
98    pub fn as_str(&self) -> &str {
99        match self {
100            HttpMethod::Get => "GET",
101            HttpMethod::Put => "PUT",
102            HttpMethod::Post => "POST",
103            HttpMethod::Delete => "DELETE",
104            HttpMethod::Options => "OPTIONS",
105            HttpMethod::Head => "HEAD",
106            HttpMethod::Patch => "PATCH",
107            HttpMethod::Trace => "TRACE",
108            HttpMethod::Other(s) => s,
109        }
110    }
111}
112
113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
114pub struct Parameter {
115    pub name: String,
116    #[serde(rename = "type")]
117    pub r#type: TypeRef,
118    pub required: bool,
119    /// OAS §4.12 `description` (CommonMark).
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub description: Option<String>,
122    /// OAS §4.12 `deprecated`.
123    #[serde(default, skip_serializing_if = "crate::is_false")]
124    pub deprecated: bool,
125    /// OAS §4.12 `example` / `examples`. Named entries; 3.0 bare
126    /// `example` lands under the synthetic key `"_default"`.
127    #[serde(default, skip_serializing_if = "Vec::is_empty")]
128    pub examples: Vec<(String, crate::Example)>,
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub style: Option<ParameterStyle>,
131    #[serde(default)]
132    pub explode: bool,
133    /// OAS `allowEmptyValue`. Permits `?foo=` with no value. Only legal
134    /// on `in: query`; the parser warns and clears it for other
135    /// locations.
136    #[serde(default)]
137    pub allow_empty_value: bool,
138    /// OAS `allowReserved`. Permits raw RFC 3986 reserved chars in the
139    /// rendered query string. Defaults to `false`.
140    #[serde(default)]
141    pub allow_reserved: bool,
142    /// `x-*` extensions declared on the parameter. Compound extensions
143    /// drop with `parser/W-EXTENSION-DROPPED`.
144    #[serde(default, skip_serializing_if = "Vec::is_empty")]
145    pub extensions: Vec<(String, ValueRef)>,
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    pub location: Option<SpecLocation>,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
151#[serde(rename_all = "kebab-case")]
152pub enum ParameterStyle {
153    Form,
154    Simple,
155    Label,
156    Matrix,
157    SpaceDelimited,
158    PipeDelimited,
159    DeepObject,
160}
161
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
163pub struct Body {
164    pub content: Vec<BodyContent>,
165    pub required: bool,
166    /// OAS §4.13 `description` (CommonMark).
167    #[serde(default, skip_serializing_if = "Option::is_none")]
168    pub description: Option<String>,
169    /// `x-*` extensions declared on the request-body object. Compound
170    /// extensions drop with `parser/W-EXTENSION-DROPPED`.
171    #[serde(default, skip_serializing_if = "Vec::is_empty")]
172    pub extensions: Vec<(String, ValueRef)>,
173}
174
175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
176pub struct BodyContent {
177    pub media_type: String,
178    #[serde(rename = "type")]
179    pub r#type: TypeRef,
180    #[serde(default, skip_serializing_if = "Vec::is_empty")]
181    pub encoding: Vec<(String, Encoding)>,
182    /// OAS 3.2 `itemSchema`: per-item shape for sequence-of-items
183    /// responses (JSON Lines, SSE event-stream, multipart/mixed).
184    /// Mutually exclusive with `schema` in the spec; when present, the
185    /// parser populates `type` with the same ref so generators that
186    /// don't model streaming see a usable type. Streaming-aware
187    /// generators read `item_schema` to know they should decode one
188    /// record at a time.
189    #[serde(default, skip_serializing_if = "Option::is_none")]
190    pub item_schema: Option<TypeRef>,
191    /// OAS §4.14 `example` / `examples`. The Media Type Object has no
192    /// `description` or `summary` field per spec — body-level prose
193    /// belongs on the surrounding RequestBody / Response.
194    #[serde(default, skip_serializing_if = "Vec::is_empty")]
195    pub examples: Vec<(String, crate::Example)>,
196    /// `x-*` extensions declared on the media-type object. Compound
197    /// extensions drop with `parser/W-EXTENSION-DROPPED`.
198    #[serde(default, skip_serializing_if = "Vec::is_empty")]
199    pub extensions: Vec<(String, ValueRef)>,
200}
201
202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
203pub struct Encoding {
204    #[serde(default, skip_serializing_if = "Option::is_none")]
205    pub content_type: Option<String>,
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub style: Option<ParameterStyle>,
208    #[serde(default)]
209    pub explode: bool,
210    /// OAS `allowReserved`. When `true`, RFC 3986 reserved characters
211    /// (`:/?#[]@!$&'()*+,;=`) are passed through verbatim instead of
212    /// being percent-encoded; meaningful for
213    /// `application/x-www-form-urlencoded` parts. Defaults to `false`.
214    #[serde(default)]
215    pub allow_reserved: bool,
216    #[serde(default, skip_serializing_if = "Vec::is_empty")]
217    pub headers: Vec<(String, Header)>,
218    /// `x-*` extensions declared on the encoding object. Compound
219    /// extensions drop with `parser/W-EXTENSION-DROPPED`.
220    #[serde(default, skip_serializing_if = "Vec::is_empty")]
221    pub extensions: Vec<(String, ValueRef)>,
222}
223
224/// OAS Header Object. Distinct from [`Parameter`] because headers
225/// have a fixed serialization style (no `style`/`explode`), and
226/// because `required` carries different semantics here:
227/// "documented as always present in the response" rather than "the
228/// request must include this".
229///
230/// The header's name lives on the surrounding tuple key
231/// (`Vec<(String, Header)>`), not on this struct.
232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233pub struct Header {
234    #[serde(rename = "type")]
235    pub r#type: TypeRef,
236    #[serde(default)]
237    pub required: bool,
238    /// OAS §4.21 `description` (CommonMark).
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    pub description: Option<String>,
241    /// OAS §4.21 `deprecated`.
242    #[serde(default, skip_serializing_if = "crate::is_false")]
243    pub deprecated: bool,
244    /// OAS §4.21 `example` / `examples`.
245    #[serde(default, skip_serializing_if = "Vec::is_empty")]
246    pub examples: Vec<(String, crate::Example)>,
247    /// OAS Header Object inherits the Parameter Object's serialization
248    /// fields. The spec fixes `style` to `simple` for headers, but
249    /// captures the value verbatim so spec-strict consumers
250    /// (validators, doc generators) can see what was declared. Most
251    /// generators ignore this slot.
252    #[serde(default, skip_serializing_if = "Option::is_none")]
253    pub style: Option<ParameterStyle>,
254    /// OAS `explode`. Defaults to `false` per Header Object semantics.
255    #[serde(default)]
256    pub explode: bool,
257    /// OAS `allowReserved`. Permits raw RFC 3986 reserved characters
258    /// in the rendered header value. Defaults to `false`.
259    #[serde(default)]
260    pub allow_reserved: bool,
261    /// OAS `allowEmptyValue`. Defaults to `false`.
262    #[serde(default)]
263    pub allow_empty_value: bool,
264    #[serde(default, skip_serializing_if = "Option::is_none")]
265    pub location: Option<SpecLocation>,
266}
267
268#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
269pub struct Response {
270    pub status: ResponseStatus,
271    #[serde(default, skip_serializing_if = "Vec::is_empty")]
272    pub content: Vec<BodyContent>,
273    #[serde(default, skip_serializing_if = "Vec::is_empty")]
274    pub headers: Vec<(String, Header)>,
275    /// OAS 3.2 §4.17 `summary` — short label, new in 3.2.
276    #[serde(default, skip_serializing_if = "Option::is_none")]
277    pub summary: Option<String>,
278    /// OAS §4.17 `description` (CommonMark). Required in 3.0 / 3.1;
279    /// optional in 3.2.
280    #[serde(default, skip_serializing_if = "Option::is_none")]
281    pub description: Option<String>,
282    /// OAS `links` — HATEOAS follow-ups. Named entries; order is
283    /// preserved.
284    #[serde(default, skip_serializing_if = "Vec::is_empty")]
285    pub links: Vec<(String, crate::Link)>,
286    /// `x-*` extensions declared on the response object. Compound
287    /// extensions drop with `parser/W-EXTENSION-DROPPED`.
288    #[serde(default, skip_serializing_if = "Vec::is_empty")]
289    pub extensions: Vec<(String, ValueRef)>,
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
293#[serde(tag = "kind", rename_all = "kebab-case")]
294pub enum ResponseStatus {
295    Explicit {
296        code: u16,
297    },
298    Default,
299    /// A status range from `1` (1xx) through `5` (5xx).
300    Range {
301        class: u8,
302    },
303}