1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[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#[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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct RequestBody {
70 pub required: bool,
71 pub content: HashMap<String, MediaType>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct MediaType {
77 pub schema: SchemaRef,
78}
79
80#[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#[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 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 pub fn description(mut self, desc: impl Into<String>) -> Self {
115 self.info.description = Some(desc.into());
116 self
117 }
118
119 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 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 pub fn register<T: for<'a> utoipa::ToSchema<'a>>(mut self) -> Self {
141 let (name, schema) = T::schema(); 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 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 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 pub fn summary(mut self, summary: impl Into<String>) -> Self {
188 self.summary = Some(summary.into());
189 self
190 }
191
192 pub fn description(mut self, desc: impl Into<String>) -> Self {
194 self.description = Some(desc.into());
195 self
196 }
197
198 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
211pub trait OperationModifier {
216 fn update_operation(op: &mut Operation);
218}
219
220impl<T: OperationModifier> OperationModifier for Option<T> {
222 fn update_operation(op: &mut Operation) {
223 T::update_operation(op);
224 if let Some(body) = &mut op.request_body {
226 body.required = false;
227 }
228 }
229}
230
231impl<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
238macro_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
253pub trait ResponseModifier {
255 fn update_response(op: &mut Operation);
257}
258
259impl 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
270impl 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
289impl 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
308impl<T: ResponseModifier> ResponseModifier for Option<T> {
310 fn update_response(op: &mut Operation) {
311 T::update_response(op);
312 }
313}
314
315impl<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
323impl<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}