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(), ResponseSpec {
178                    description: "Successful response".to_string(),
179                    content: None,
180                })
181            ]),
182        }
183    }
184
185    /// Set summary
186    pub fn summary(mut self, summary: impl Into<String>) -> Self {
187        self.summary = Some(summary.into());
188        self
189    }
190
191    /// Set description
192    pub fn description(mut self, desc: impl Into<String>) -> Self {
193        self.description = Some(desc.into());
194        self
195    }
196
197    /// Add tags
198    pub fn tags(mut self, tags: Vec<String>) -> Self {
199        self.tags = Some(tags);
200        self
201    }
202}
203
204impl Default for Operation {
205    fn default() -> Self {
206        Self::new()
207    }
208}
209
210/// Trait for types that can modify an OpenAPI operation
211///
212/// This is used by extractors to automatically update the operation
213/// documentation (e.g. adding request body schema, parameters, etc.)
214pub trait OperationModifier {
215    /// Update the operation
216    fn update_operation(op: &mut Operation);
217}
218
219// Implement for Option<T>
220impl<T: OperationModifier> OperationModifier for Option<T> {
221    fn update_operation(op: &mut Operation) {
222        T::update_operation(op);
223        // If request body was added, make it optional
224        if let Some(body) = &mut op.request_body {
225            body.required = false;
226        }
227    }
228}
229
230// Implement for Result<T, E>
231impl<T: OperationModifier, E> OperationModifier for std::result::Result<T, E> {
232    fn update_operation(op: &mut Operation) {
233        T::update_operation(op);
234    }
235}
236
237// Implement for primitives (no-op)
238macro_rules! impl_op_modifier_for_primitives {
239    ($($ty:ty),*) => {
240        $(
241            impl OperationModifier for $ty {
242                fn update_operation(_op: &mut Operation) {}
243            }
244        )*
245    };
246}
247
248impl_op_modifier_for_primitives!(
249    i8, i16, i32, i64, i128, isize,
250    u8, u16, u32, u64, u128, usize,
251    f32, f64,
252    bool,
253    String
254);
255
256// ResponseModifier trait
257pub trait ResponseModifier {
258    /// Update the operation with response information
259    fn update_response(op: &mut Operation);
260}
261
262// Implement for () - 200 OK (empty)
263impl ResponseModifier for () {
264    fn update_response(op: &mut Operation) {
265        let response = ResponseSpec {
266            description: "Successful response".to_string(),
267            ..Default::default()
268        };
269        op.responses.insert("200".to_string(), response);
270    }
271}
272
273// Implement for String - 200 OK (text/plain)
274impl ResponseModifier for String {
275    fn update_response(op: &mut Operation) {
276        let mut content = std::collections::HashMap::new();
277        content.insert("text/plain".to_string(), MediaType {
278            schema: SchemaRef::Inline(serde_json::json!({ "type": "string" })),
279        });
280        
281        let response = ResponseSpec {
282            description: "Successful response".to_string(),
283            content: Some(content),
284            ..Default::default()
285        };
286        op.responses.insert("200".to_string(), response);
287    }
288}
289
290// Implement for &'static str - 200 OK (text/plain)
291impl ResponseModifier for &'static str {
292    fn update_response(op: &mut Operation) {
293        let mut content = std::collections::HashMap::new();
294        content.insert("text/plain".to_string(), MediaType {
295            schema: SchemaRef::Inline(serde_json::json!({ "type": "string" })),
296        });
297        
298        let response = ResponseSpec {
299            description: "Successful response".to_string(),
300            content: Some(content),
301            ..Default::default()
302        };
303        op.responses.insert("200".to_string(), response);
304    }
305}
306
307// Implement for Option<T> - Delegates to T
308impl<T: ResponseModifier> ResponseModifier for Option<T> {
309    fn update_response(op: &mut Operation) {
310        T::update_response(op);
311    }
312}
313
314// Implement for Result<T, E> - Delegates to T (success) and E (error)
315impl<T: ResponseModifier, E: ResponseModifier> ResponseModifier for Result<T, E> {
316    fn update_response(op: &mut Operation) {
317        T::update_response(op);
318        E::update_response(op);
319    }
320}
321
322// Implement for http::Response<T> - Generic 200 OK
323impl<T> ResponseModifier for http::Response<T> {
324    fn update_response(op: &mut Operation) {
325        op.responses.insert("200".to_string(), ResponseSpec {
326            description: "Successful response".to_string(),
327            ..Default::default()
328        });
329    }
330}
331