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(), ResponseSpec {
178 description: "Successful response".to_string(),
179 content: None,
180 })
181 ]),
182 }
183 }
184
185 pub fn summary(mut self, summary: impl Into<String>) -> Self {
187 self.summary = Some(summary.into());
188 self
189 }
190
191 pub fn description(mut self, desc: impl Into<String>) -> Self {
193 self.description = Some(desc.into());
194 self
195 }
196
197 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
210pub trait OperationModifier {
215 fn update_operation(op: &mut Operation);
217}
218
219impl<T: OperationModifier> OperationModifier for Option<T> {
221 fn update_operation(op: &mut Operation) {
222 T::update_operation(op);
223 if let Some(body) = &mut op.request_body {
225 body.required = false;
226 }
227 }
228}
229
230impl<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
237macro_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
256pub trait ResponseModifier {
258 fn update_response(op: &mut Operation);
260}
261
262impl 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
273impl 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
290impl 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
307impl<T: ResponseModifier> ResponseModifier for Option<T> {
309 fn update_response(op: &mut Operation) {
310 T::update_response(op);
311 }
312}
313
314impl<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
322impl<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