Skip to main content

oa_forge_parser/
openapi.rs

1use indexmap::IndexMap;
2use serde::Deserialize;
3
4/// Top-level OpenAPI 3.0/3.1 specification.
5#[derive(Debug, Deserialize)]
6pub struct OpenApiSpec {
7    pub openapi: String,
8    pub info: Info,
9    #[serde(default)]
10    pub paths: IndexMap<String, PathItem>,
11    #[serde(default)]
12    pub components: Option<Components>,
13}
14
15#[derive(Debug, Deserialize)]
16pub struct Info {
17    pub title: String,
18    pub version: String,
19    #[serde(default)]
20    pub description: Option<String>,
21}
22
23#[derive(Debug, Clone, Deserialize)]
24pub struct PathItem {
25    #[serde(default)]
26    pub get: Option<Operation>,
27    #[serde(default)]
28    pub post: Option<Operation>,
29    #[serde(default)]
30    pub put: Option<Operation>,
31    #[serde(default)]
32    pub patch: Option<Operation>,
33    #[serde(default)]
34    pub delete: Option<Operation>,
35    #[serde(default)]
36    pub parameters: Vec<ParameterOrRef>,
37}
38
39impl PathItem {
40    /// Iterate over all (method_name, operation) pairs defined on this path.
41    pub fn operations(&self) -> impl Iterator<Item = (&'static str, &Operation)> {
42        [
43            ("get", &self.get),
44            ("post", &self.post),
45            ("put", &self.put),
46            ("patch", &self.patch),
47            ("delete", &self.delete),
48        ]
49        .into_iter()
50        .filter_map(|(method, op)| op.as_ref().map(|o| (method, o)))
51    }
52
53    /// Iterate over mutable references to all operations.
54    pub fn operations_mut(&mut self) -> impl Iterator<Item = (&'static str, &mut Operation)> {
55        [
56            ("get", &mut self.get),
57            ("post", &mut self.post),
58            ("put", &mut self.put),
59            ("patch", &mut self.patch),
60            ("delete", &mut self.delete),
61        ]
62        .into_iter()
63        .filter_map(|(method, op)| op.as_mut().map(|o| (method, o)))
64    }
65}
66
67#[derive(Debug, Clone, Deserialize)]
68#[serde(rename_all = "camelCase")]
69pub struct Operation {
70    #[serde(default)]
71    pub operation_id: Option<String>,
72    #[serde(default)]
73    pub summary: Option<String>,
74    #[serde(default)]
75    pub tags: Vec<String>,
76    #[serde(default)]
77    pub parameters: Vec<ParameterOrRef>,
78    #[serde(default)]
79    pub request_body: Option<RequestBodyOrRef>,
80    #[serde(default)]
81    pub responses: IndexMap<String, ResponseOrRef>,
82}
83
84#[derive(Debug, Clone, Deserialize)]
85pub struct Parameter {
86    pub name: String,
87    #[serde(rename = "in")]
88    pub location: String,
89    #[serde(default)]
90    pub required: bool,
91    #[serde(default)]
92    pub schema: Option<SchemaOrRef>,
93    #[serde(default)]
94    pub description: Option<String>,
95    #[serde(default)]
96    pub style: Option<String>,
97    #[serde(default)]
98    pub explode: Option<bool>,
99}
100
101/// A parameter that can be inline or a $ref to components/parameters.
102#[derive(Debug, Clone, Deserialize)]
103#[serde(untagged)]
104pub enum ParameterOrRef {
105    Ref {
106        #[serde(rename = "$ref")]
107        ref_path: String,
108    },
109    Parameter(Parameter),
110}
111
112#[derive(Debug, Clone, Deserialize)]
113pub struct RequestBody {
114    #[serde(default)]
115    pub required: bool,
116    #[serde(default)]
117    pub content: IndexMap<String, MediaType>,
118    #[serde(default)]
119    pub description: Option<String>,
120}
121
122/// A request body that can be inline or a $ref to components/requestBodies.
123#[derive(Debug, Clone, Deserialize)]
124#[serde(untagged)]
125pub enum RequestBodyOrRef {
126    Ref {
127        #[serde(rename = "$ref")]
128        ref_path: String,
129    },
130    RequestBody(RequestBody),
131}
132
133#[derive(Debug, Clone, Deserialize)]
134pub struct Response {
135    #[serde(default)]
136    pub description: Option<String>,
137    #[serde(default)]
138    pub content: Option<IndexMap<String, MediaType>>,
139}
140
141/// A response that can be inline or a $ref to components/responses.
142#[derive(Debug, Clone, Deserialize)]
143#[serde(untagged)]
144pub enum ResponseOrRef {
145    Ref {
146        #[serde(rename = "$ref")]
147        ref_path: String,
148    },
149    Response(Response),
150}
151
152#[derive(Debug, Clone, Deserialize)]
153pub struct MediaType {
154    #[serde(default)]
155    pub schema: Option<SchemaOrRef>,
156}
157
158/// A schema that can be either inline or a $ref.
159#[derive(Debug, Clone, Deserialize)]
160#[serde(untagged)]
161pub enum SchemaOrRef {
162    Ref {
163        #[serde(rename = "$ref")]
164        ref_path: String,
165    },
166    Schema(Box<Schema>),
167}
168
169/// OpenAPI 3.1 allows `type` to be either a string or an array of strings.
170/// e.g. `type: "string"` (3.0) or `type: ["string", "null"]` (3.1)
171#[derive(Debug, Clone, Deserialize)]
172#[serde(untagged)]
173pub enum SchemaType {
174    Single(String),
175    Array(Vec<String>),
176}
177
178#[derive(Debug, Clone, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct Schema {
181    #[serde(rename = "type", default)]
182    pub schema_type: Option<SchemaType>,
183    #[serde(default)]
184    pub format: Option<String>,
185    #[serde(default)]
186    pub properties: IndexMap<String, SchemaOrRef>,
187    #[serde(default)]
188    pub required: Vec<String>,
189    #[serde(default)]
190    pub items: Option<Box<SchemaOrRef>>,
191    /// OpenAPI 3.1 tuple type: `prefixItems: [{type: string}, {type: number}]`
192    #[serde(default)]
193    pub prefix_items: Option<Vec<SchemaOrRef>>,
194    #[serde(rename = "enum", default)]
195    pub enum_values: Vec<serde_json::Value>,
196    #[serde(default)]
197    pub all_of: Option<Vec<SchemaOrRef>>,
198    #[serde(default)]
199    pub one_of: Option<Vec<SchemaOrRef>>,
200    #[serde(default)]
201    pub any_of: Option<Vec<SchemaOrRef>>,
202    #[serde(default)]
203    pub discriminator: Option<Discriminator>,
204    #[serde(default)]
205    pub additional_properties: Option<AdditionalProperties>,
206    #[serde(default)]
207    pub nullable: bool,
208    #[serde(default)]
209    pub read_only: bool,
210    #[serde(default)]
211    pub description: Option<String>,
212    #[serde(default)]
213    pub default: Option<serde_json::Value>,
214    // Validation constraints (JSON Schema)
215    #[serde(default)]
216    pub min_length: Option<u64>,
217    #[serde(default)]
218    pub max_length: Option<u64>,
219    #[serde(default)]
220    pub pattern: Option<String>,
221    #[serde(default)]
222    pub minimum: Option<f64>,
223    #[serde(default)]
224    pub maximum: Option<f64>,
225    #[serde(default)]
226    pub exclusive_minimum: Option<f64>,
227    #[serde(default)]
228    pub exclusive_maximum: Option<f64>,
229    #[serde(default)]
230    pub multiple_of: Option<f64>,
231    #[serde(default)]
232    pub min_items: Option<u64>,
233    #[serde(default)]
234    pub max_items: Option<u64>,
235    /// OpenAPI 3.1: `$defs` for local schema definitions (JSON Schema Draft 2020-12)
236    #[serde(rename = "$defs", default)]
237    pub defs: Option<IndexMap<String, SchemaOrRef>>,
238}
239
240/// `additionalProperties` can be a boolean or a schema.
241/// `true` → Record<string, unknown>, `false` → no extra props, schema → Record<string, T>
242#[derive(Debug, Clone, Deserialize)]
243#[serde(untagged)]
244pub enum AdditionalProperties {
245    Bool(bool),
246    Schema(Box<SchemaOrRef>),
247}
248
249#[derive(Debug, Clone, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub struct Discriminator {
252    pub property_name: String,
253    #[serde(default)]
254    pub mapping: IndexMap<String, String>,
255}
256
257#[derive(Debug, Deserialize)]
258#[serde(rename_all = "camelCase")]
259pub struct Components {
260    #[serde(default)]
261    pub schemas: IndexMap<String, SchemaOrRef>,
262    #[serde(default)]
263    pub parameters: IndexMap<String, ParameterOrRef>,
264    #[serde(default)]
265    pub request_bodies: IndexMap<String, RequestBodyOrRef>,
266    #[serde(default)]
267    pub responses: IndexMap<String, ResponseOrRef>,
268}