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();
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 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 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 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 pub fn summary(mut self, summary: impl Into<String>) -> Self {
198 self.summary = Some(summary.into());
199 self
200 }
201
202 pub fn description(mut self, desc: impl Into<String>) -> Self {
204 self.description = Some(desc.into());
205 self
206 }
207
208 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
221pub trait OperationModifier {
226 fn update_operation(op: &mut Operation);
228}
229
230impl<T: OperationModifier> OperationModifier for Option<T> {
232 fn update_operation(op: &mut Operation) {
233 T::update_operation(op);
234 if let Some(body) = &mut op.request_body {
236 body.required = false;
237 }
238 }
239}
240
241impl<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
248macro_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
263pub trait ResponseModifier {
265 fn update_response(op: &mut Operation);
267}
268
269impl 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
280impl 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
299impl 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
318impl<T: ResponseModifier> ResponseModifier for Option<T> {
320 fn update_response(op: &mut Operation) {
321 T::update_response(op);
322 }
323}
324
325impl<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
333impl<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}