rustapi_openapi/
spec.rs

1//! OpenAPI specification types
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// API information for OpenAPI spec
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ApiInfo {
9    pub title: String,
10    pub version: String,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub description: Option<String>,
13}
14
15/// OpenAPI specification builder
16#[derive(Debug, Clone)]
17pub struct OpenApiSpec {
18    pub info: ApiInfo,
19    pub paths: HashMap<String, PathItem>,
20    pub schemas: HashMap<String, serde_json::Value>,
21}
22
23/// Path item in OpenAPI spec
24#[derive(Debug, Clone, Serialize, Deserialize, Default)]
25pub struct PathItem {
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub get: Option<Operation>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub post: Option<Operation>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub put: Option<Operation>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub patch: Option<Operation>,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub delete: Option<Operation>,
36}
37
38/// Operation (endpoint) in OpenAPI spec
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct Operation {
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub summary: Option<String>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub description: Option<String>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub tags: Option<Vec<String>>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub parameters: Option<Vec<Parameter>>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    #[serde(rename = "requestBody")]
51    pub request_body: Option<RequestBody>,
52    pub responses: HashMap<String, ResponseSpec>,
53}
54
55/// Parameter in OpenAPI spec
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct Parameter {
58    pub name: String,
59    #[serde(rename = "in")]
60    pub location: String,
61    pub required: bool,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub description: Option<String>,
64    pub schema: SchemaRef,
65}
66
67/// Request body in OpenAPI spec
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct RequestBody {
70    pub required: bool,
71    pub content: HashMap<String, MediaType>,
72}
73
74/// Media type in OpenAPI spec
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct MediaType {
77    pub schema: SchemaRef,
78}
79
80/// Response specification
81#[derive(Debug, Clone, Serialize, Deserialize, Default)]
82pub struct ResponseSpec {
83    pub description: String,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub content: Option<HashMap<String, MediaType>>,
86}
87
88/// Schema reference or inline schema
89#[derive(Debug, Clone, Serialize, Deserialize)]
90#[serde(untagged)]
91pub enum SchemaRef {
92    Ref {
93        #[serde(rename = "$ref")]
94        reference: String,
95    },
96    Inline(serde_json::Value),
97}
98
99impl OpenApiSpec {
100    /// Create a new OpenAPI specification
101    pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
102        Self {
103            info: ApiInfo {
104                title: title.into(),
105                version: version.into(),
106                description: None,
107            },
108            paths: HashMap::new(),
109            schemas: HashMap::new(),
110        }
111    }
112
113    /// Set description
114    pub fn description(mut self, desc: impl Into<String>) -> Self {
115        self.info.description = Some(desc.into());
116        self
117    }
118
119    /// Add a path operation
120    pub fn path(mut self, path: &str, method: &str, operation: Operation) -> Self {
121        let item = self.paths.entry(path.to_string()).or_default();
122        match method.to_uppercase().as_str() {
123            "GET" => item.get = Some(operation),
124            "POST" => item.post = Some(operation),
125            "PUT" => item.put = Some(operation),
126            "PATCH" => item.patch = Some(operation),
127            "DELETE" => item.delete = Some(operation),
128            _ => {}
129        }
130        self
131    }
132
133    /// Add a schema definition
134    pub fn schema(mut self, name: &str, schema: serde_json::Value) -> Self {
135        self.schemas.insert(name.to_string(), schema);
136        self
137    }
138
139    /// Register a type that implements Schema (utoipa::ToSchema)
140    pub fn register<T: for<'a> utoipa::ToSchema<'a>>(mut self) -> Self {
141        let (name, schema) = T::schema(); // returns (Cow<str>, RefOr<Schema>)
142                                          // Convert to JSON value
143        if let Ok(json_schema) = serde_json::to_value(schema) {
144            self.schemas.insert(name.to_string(), json_schema);
145        }
146        self
147    }
148
149    /// Convert to JSON value
150    pub fn to_json(&self) -> serde_json::Value {
151        let mut spec = serde_json::json!({
152            "openapi": "3.0.3",
153            "info": self.info,
154            "paths": self.paths,
155        });
156
157        if !self.schemas.is_empty() {
158            spec["components"] = serde_json::json!({
159                "schemas": self.schemas
160            });
161        }
162
163        spec
164    }
165}
166
167impl Operation {
168    /// Create a new operation
169    pub fn new() -> Self {
170        Self {
171            summary: None,
172            description: None,
173            tags: None,
174            parameters: None,
175            request_body: None,
176            responses: HashMap::from([(
177                "200".to_string(),
178                ResponseSpec {
179                    description: "Successful response".to_string(),
180                    content: None,
181                },
182            )]),
183        }
184    }
185
186    /// Set summary
187    pub fn summary(mut self, summary: impl Into<String>) -> Self {
188        self.summary = Some(summary.into());
189        self
190    }
191
192    /// Set description
193    pub fn description(mut self, desc: impl Into<String>) -> Self {
194        self.description = Some(desc.into());
195        self
196    }
197
198    /// Add tags
199    pub fn tags(mut self, tags: Vec<String>) -> Self {
200        self.tags = Some(tags);
201        self
202    }
203}
204
205impl Default for Operation {
206    fn default() -> Self {
207        Self::new()
208    }
209}
210
211/// Trait for types that can modify an OpenAPI operation
212///
213/// This is used by extractors to automatically update the operation
214/// documentation (e.g. adding request body schema, parameters, etc.)
215pub trait OperationModifier {
216    /// Update the operation
217    fn update_operation(op: &mut Operation);
218}
219
220// Implement for Option<T>
221impl<T: OperationModifier> OperationModifier for Option<T> {
222    fn update_operation(op: &mut Operation) {
223        T::update_operation(op);
224        // If request body was added, make it optional
225        if let Some(body) = &mut op.request_body {
226            body.required = false;
227        }
228    }
229}
230
231// Implement for Result<T, E>
232impl<T: OperationModifier, E> OperationModifier for std::result::Result<T, E> {
233    fn update_operation(op: &mut Operation) {
234        T::update_operation(op);
235    }
236}
237
238// Implement for primitives (no-op)
239macro_rules! impl_op_modifier_for_primitives {
240    ($($ty:ty),*) => {
241        $(
242            impl OperationModifier for $ty {
243                fn update_operation(_op: &mut Operation) {}
244            }
245        )*
246    };
247}
248
249impl_op_modifier_for_primitives!(
250    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool, String
251);
252
253// ResponseModifier trait
254pub trait ResponseModifier {
255    /// Update the operation with response information
256    fn update_response(op: &mut Operation);
257}
258
259// Implement for () - 200 OK (empty)
260impl ResponseModifier for () {
261    fn update_response(op: &mut Operation) {
262        let response = ResponseSpec {
263            description: "Successful response".to_string(),
264            ..Default::default()
265        };
266        op.responses.insert("200".to_string(), response);
267    }
268}
269
270// Implement for String - 200 OK (text/plain)
271impl ResponseModifier for String {
272    fn update_response(op: &mut Operation) {
273        let mut content = std::collections::HashMap::new();
274        content.insert(
275            "text/plain".to_string(),
276            MediaType {
277                schema: SchemaRef::Inline(serde_json::json!({ "type": "string" })),
278            },
279        );
280
281        let response = ResponseSpec {
282            description: "Successful response".to_string(),
283            content: Some(content),
284        };
285        op.responses.insert("200".to_string(), response);
286    }
287}
288
289// Implement for &'static str - 200 OK (text/plain)
290impl ResponseModifier for &'static str {
291    fn update_response(op: &mut Operation) {
292        let mut content = std::collections::HashMap::new();
293        content.insert(
294            "text/plain".to_string(),
295            MediaType {
296                schema: SchemaRef::Inline(serde_json::json!({ "type": "string" })),
297            },
298        );
299
300        let response = ResponseSpec {
301            description: "Successful response".to_string(),
302            content: Some(content),
303        };
304        op.responses.insert("200".to_string(), response);
305    }
306}
307
308// Implement for Option<T> - Delegates to T
309impl<T: ResponseModifier> ResponseModifier for Option<T> {
310    fn update_response(op: &mut Operation) {
311        T::update_response(op);
312    }
313}
314
315// Implement for Result<T, E> - Delegates to T (success) and E (error)
316impl<T: ResponseModifier, E: ResponseModifier> ResponseModifier for Result<T, E> {
317    fn update_response(op: &mut Operation) {
318        T::update_response(op);
319        E::update_response(op);
320    }
321}
322
323// Implement for http::Response<T> - Generic 200 OK
324impl<T> ResponseModifier for http::Response<T> {
325    fn update_response(op: &mut Operation) {
326        op.responses.insert(
327            "200".to_string(),
328            ResponseSpec {
329                description: "Successful response".to_string(),
330                ..Default::default()
331            },
332        );
333    }
334}