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