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();
142        if let Ok(json_schema) = serde_json::to_value(schema) {
143            self.schemas.insert(name.to_string(), json_schema);
144        }
145        self
146    }
147
148    /// Register a type into this spec in-place.
149    ///
150    /// This is useful for zero-config registration paths where the spec is stored
151    /// by value in another struct (e.g., the application builder).
152    pub fn register_in_place<T: for<'a> utoipa::ToSchema<'a>>(&mut self) {
153        let (name, schema) = T::schema();
154        if let Ok(json_schema) = serde_json::to_value(schema) {
155            self.schemas.insert(name.to_string(), json_schema);
156        }
157    }
158
159    /// Convert to JSON value
160    pub fn to_json(&self) -> serde_json::Value {
161        let mut spec = serde_json::json!({
162            "openapi": "3.0.3",
163            "info": self.info,
164            "paths": self.paths,
165        });
166
167        if !self.schemas.is_empty() {
168            spec["components"] = serde_json::json!({
169                "schemas": self.schemas
170            });
171        }
172
173        spec
174    }
175}
176
177impl Operation {
178    /// Create a new operation
179    pub fn new() -> Self {
180        Self {
181            summary: None,
182            description: None,
183            tags: None,
184            parameters: None,
185            request_body: None,
186            responses: HashMap::from([(
187                "200".to_string(),
188                ResponseSpec {
189                    description: "Successful response".to_string(),
190                    content: None,
191                },
192            )]),
193        }
194    }
195
196    /// Set summary
197    pub fn summary(mut self, summary: impl Into<String>) -> Self {
198        self.summary = Some(summary.into());
199        self
200    }
201
202    /// Set description
203    pub fn description(mut self, desc: impl Into<String>) -> Self {
204        self.description = Some(desc.into());
205        self
206    }
207
208    /// Add tags
209    pub fn tags(mut self, tags: Vec<String>) -> Self {
210        self.tags = Some(tags);
211        self
212    }
213}
214
215impl Default for Operation {
216    fn default() -> Self {
217        Self::new()
218    }
219}
220
221/// Trait for types that can modify an OpenAPI operation
222///
223/// This is used by extractors to automatically update the operation
224/// documentation (e.g. adding request body schema, parameters, etc.)
225pub trait OperationModifier {
226    /// Update the operation
227    fn update_operation(op: &mut Operation);
228}
229
230// Implement for Option<T>
231impl<T: OperationModifier> OperationModifier for Option<T> {
232    fn update_operation(op: &mut Operation) {
233        T::update_operation(op);
234        // If request body was added, make it optional
235        if let Some(body) = &mut op.request_body {
236            body.required = false;
237        }
238    }
239}
240
241// Implement for Result<T, E>
242impl<T: OperationModifier, E> OperationModifier for std::result::Result<T, E> {
243    fn update_operation(op: &mut Operation) {
244        T::update_operation(op);
245    }
246}
247
248// Implement for primitives (no-op)
249macro_rules! impl_op_modifier_for_primitives {
250    ($($ty:ty),*) => {
251        $(
252            impl OperationModifier for $ty {
253                fn update_operation(_op: &mut Operation) {}
254            }
255        )*
256    };
257}
258
259impl_op_modifier_for_primitives!(
260    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool, String
261);
262
263// ResponseModifier trait
264pub trait ResponseModifier {
265    /// Update the operation with response information
266    fn update_response(op: &mut Operation);
267}
268
269// Implement for () - 200 OK (empty)
270impl ResponseModifier for () {
271    fn update_response(op: &mut Operation) {
272        let response = ResponseSpec {
273            description: "Successful response".to_string(),
274            ..Default::default()
275        };
276        op.responses.insert("200".to_string(), response);
277    }
278}
279
280// Implement for String - 200 OK (text/plain)
281impl ResponseModifier for String {
282    fn update_response(op: &mut Operation) {
283        let mut content = std::collections::HashMap::new();
284        content.insert(
285            "text/plain".to_string(),
286            MediaType {
287                schema: SchemaRef::Inline(serde_json::json!({ "type": "string" })),
288            },
289        );
290
291        let response = ResponseSpec {
292            description: "Successful response".to_string(),
293            content: Some(content),
294        };
295        op.responses.insert("200".to_string(), response);
296    }
297}
298
299// Implement for &'static str - 200 OK (text/plain)
300impl ResponseModifier for &'static str {
301    fn update_response(op: &mut Operation) {
302        let mut content = std::collections::HashMap::new();
303        content.insert(
304            "text/plain".to_string(),
305            MediaType {
306                schema: SchemaRef::Inline(serde_json::json!({ "type": "string" })),
307            },
308        );
309
310        let response = ResponseSpec {
311            description: "Successful response".to_string(),
312            content: Some(content),
313        };
314        op.responses.insert("200".to_string(), response);
315    }
316}
317
318// Implement for Option<T> - Delegates to T
319impl<T: ResponseModifier> ResponseModifier for Option<T> {
320    fn update_response(op: &mut Operation) {
321        T::update_response(op);
322    }
323}
324
325// Implement for Result<T, E> - Delegates to T (success) and E (error)
326impl<T: ResponseModifier, E: ResponseModifier> ResponseModifier for Result<T, E> {
327    fn update_response(op: &mut Operation) {
328        T::update_response(op);
329        E::update_response(op);
330    }
331}
332
333// Implement for http::Response<T> - Generic 200 OK
334impl<T> ResponseModifier for http::Response<T> {
335    fn update_response(op: &mut Operation) {
336        op.responses.insert(
337            "200".to_string(),
338            ResponseSpec {
339                description: "Successful response".to_string(),
340                ..Default::default()
341            },
342        );
343    }
344}