1use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6use crate::schema::JsonSchema2020;
7pub use crate::schema::SchemaRef;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct OpenApiSpec {
13 pub openapi: String,
15
16 pub info: ApiInfo,
18
19 #[serde(skip_serializing_if = "Option::is_none")]
21 pub json_schema_dialect: Option<String>,
22
23 #[serde(skip_serializing_if = "Vec::is_empty")]
25 pub servers: Vec<Server>,
26
27 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
29 pub paths: BTreeMap<String, PathItem>,
30
31 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
33 pub webhooks: BTreeMap<String, PathItem>,
34
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub components: Option<Components>,
38
39 #[serde(skip_serializing_if = "Vec::is_empty")]
41 pub security: Vec<BTreeMap<String, Vec<String>>>,
42
43 #[serde(skip_serializing_if = "Vec::is_empty")]
45 pub tags: Vec<Tag>,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub external_docs: Option<ExternalDocs>,
50}
51
52impl OpenApiSpec {
53 pub fn new(title: impl Into<String>, version: impl Into<String>) -> Self {
54 Self {
55 openapi: "3.1.0".to_string(),
56 info: ApiInfo {
57 title: title.into(),
58 version: version.into(),
59 ..Default::default()
60 },
61 json_schema_dialect: Some("https://spec.openapis.org/oas/3.1/dialect/base".to_string()),
63 servers: Vec::new(),
64 paths: BTreeMap::new(),
65 webhooks: BTreeMap::new(),
66 components: None,
67 security: Vec::new(),
68 tags: Vec::new(),
69 external_docs: None,
70 }
71 }
72
73 pub fn description(mut self, desc: impl Into<String>) -> Self {
74 self.info.description = Some(desc.into());
75 self
76 }
77
78 pub fn summary(mut self, summary: impl Into<String>) -> Self {
79 self.info.summary = Some(summary.into());
80 self
81 }
82
83 pub fn path(mut self, path: &str, method: &str, operation: Operation) -> Self {
84 let item = self.paths.entry(path.to_string()).or_default();
85 match method.to_uppercase().as_str() {
86 "GET" => item.get = Some(operation),
87 "POST" => item.post = Some(operation),
88 "PUT" => item.put = Some(operation),
89 "PATCH" => item.patch = Some(operation),
90 "DELETE" => item.delete = Some(operation),
91 "HEAD" => item.head = Some(operation),
92 "OPTIONS" => item.options = Some(operation),
93 "TRACE" => item.trace = Some(operation),
94 _ => {}
95 }
96 self
97 }
98
99 pub fn register<T: crate::schema::RustApiSchema>(mut self) -> Self {
101 self.register_in_place::<T>();
102 self
103 }
104
105 pub fn register_in_place<T: crate::schema::RustApiSchema>(&mut self) {
107 let mut ctx = crate::schema::SchemaCtx::new();
108
109 if let Some(c) = &self.components {
111 ctx.components = c.schemas.clone();
112 }
113
114 let _ = T::schema(&mut ctx);
116
117 let components = self.components.get_or_insert_with(Components::default);
119 components.schemas.extend(ctx.components);
120 }
121
122 pub fn server(mut self, server: Server) -> Self {
123 self.servers.push(server);
124 self
125 }
126
127 pub fn security_scheme(mut self, name: impl Into<String>, scheme: SecurityScheme) -> Self {
128 let components = self.components.get_or_insert_with(Components::default);
129 components
130 .security_schemes
131 .entry(name.into())
132 .or_insert(scheme);
133 self
134 }
135
136 pub fn to_json(&self) -> serde_json::Value {
137 serde_json::to_value(self).unwrap_or(serde_json::Value::Null)
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, Default)]
142#[serde(rename_all = "camelCase")]
143pub struct ApiInfo {
144 pub title: String,
145 pub version: String,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub summary: Option<String>,
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub description: Option<String>,
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub terms_of_service: Option<String>,
152 #[serde(skip_serializing_if = "Option::is_none")]
153 pub contact: Option<Contact>,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub license: Option<License>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize, Default)]
159pub struct Contact {
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub name: Option<String>,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub url: Option<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub email: Option<String>,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, Default)]
169pub struct License {
170 pub name: String,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub identifier: Option<String>,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub url: Option<String>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct Server {
179 pub url: String,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub description: Option<String>,
182 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
183 pub variables: BTreeMap<String, ServerVariable>,
184}
185
186impl Server {
187 pub fn new(url: impl Into<String>) -> Self {
188 Self {
189 url: url.into(),
190 description: None,
191 variables: BTreeMap::new(),
192 }
193 }
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct ServerVariable {
199 #[serde(rename = "enum", skip_serializing_if = "Vec::is_empty")]
200 pub enum_values: Vec<String>,
201 pub default: String,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub description: Option<String>,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize, Default)]
207pub struct PathItem {
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub summary: Option<String>,
210 #[serde(skip_serializing_if = "Option::is_none")]
211 pub description: Option<String>,
212 #[serde(skip_serializing_if = "Option::is_none")]
213 pub get: Option<Operation>,
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub put: Option<Operation>,
216 #[serde(skip_serializing_if = "Option::is_none")]
217 pub post: Option<Operation>,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub delete: Option<Operation>,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub options: Option<Operation>,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub head: Option<Operation>,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub patch: Option<Operation>,
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub trace: Option<Operation>,
228 #[serde(skip_serializing_if = "Vec::is_empty")]
229 pub servers: Vec<Server>,
230 #[serde(skip_serializing_if = "Vec::is_empty")]
231 pub parameters: Vec<Parameter>,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize, Default)]
235#[serde(rename_all = "camelCase")]
236pub struct Operation {
237 #[serde(skip_serializing_if = "Vec::is_empty")]
238 pub tags: Vec<String>,
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub summary: Option<String>,
241 #[serde(skip_serializing_if = "Option::is_none")]
242 pub description: Option<String>,
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub external_docs: Option<ExternalDocs>,
245 #[serde(skip_serializing_if = "Option::is_none")]
246 pub operation_id: Option<String>,
247 #[serde(skip_serializing_if = "Vec::is_empty")]
248 pub parameters: Vec<Parameter>,
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub request_body: Option<RequestBody>,
251 pub responses: BTreeMap<String, ResponseSpec>,
252 #[serde(skip_serializing_if = "Vec::is_empty")]
253 pub security: Vec<BTreeMap<String, Vec<String>>>,
254 #[serde(skip_serializing_if = "Option::is_none")]
255 pub deprecated: Option<bool>,
256}
257
258impl Operation {
259 pub fn new() -> Self {
260 Self {
261 responses: BTreeMap::from([("200".to_string(), ResponseSpec::default())]),
262 ..Default::default()
263 }
264 }
265
266 pub fn summary(mut self, s: impl Into<String>) -> Self {
267 self.summary = Some(s.into());
268 self
269 }
270
271 pub fn description(mut self, d: impl Into<String>) -> Self {
272 self.description = Some(d.into());
273 self
274 }
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct Parameter {
279 pub name: String,
280 #[serde(rename = "in")]
281 pub location: String,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub description: Option<String>,
284 pub required: bool,
285 #[serde(skip_serializing_if = "Option::is_none")]
286 pub deprecated: Option<bool>,
287 #[serde(skip_serializing_if = "Option::is_none")]
288 pub schema: Option<SchemaRef>,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct RequestBody {
293 #[serde(skip_serializing_if = "Option::is_none")]
294 pub description: Option<String>,
295 pub content: BTreeMap<String, MediaType>,
296 #[serde(skip_serializing_if = "Option::is_none")]
297 pub required: Option<bool>,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize, Default)]
301pub struct ResponseSpec {
302 pub description: String,
303 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
304 pub content: BTreeMap<String, MediaType>,
305 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
306 pub headers: BTreeMap<String, Header>,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct MediaType {
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub schema: Option<SchemaRef>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub example: Option<serde_json::Value>,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct Header {
319 #[serde(skip_serializing_if = "Option::is_none")]
320 pub description: Option<String>,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 pub schema: Option<SchemaRef>,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize, Default)]
326#[serde(rename_all = "camelCase")]
327pub struct Components {
328 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
329 pub schemas: BTreeMap<String, JsonSchema2020>,
330 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
331 pub responses: BTreeMap<String, ResponseSpec>,
332 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
333 pub parameters: BTreeMap<String, Parameter>,
334 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
335 pub examples: BTreeMap<String, serde_json::Value>,
336 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
337 pub request_bodies: BTreeMap<String, RequestBody>,
338 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
339 pub headers: BTreeMap<String, Header>,
340 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
341 pub security_schemes: BTreeMap<String, SecurityScheme>,
342 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
343 pub links: BTreeMap<String, serde_json::Value>,
344 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
345 pub callbacks: BTreeMap<String, BTreeMap<String, PathItem>>,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
349#[serde(tag = "type", rename_all = "camelCase")]
350pub enum SecurityScheme {
351 ApiKey {
352 name: String,
353 #[serde(rename = "in")]
354 location: String,
355 #[serde(skip_serializing_if = "Option::is_none")]
356 description: Option<String>,
357 },
358 Http {
359 scheme: String,
360 #[serde(skip_serializing_if = "Option::is_none")]
361 bearer_format: Option<String>,
362 #[serde(skip_serializing_if = "Option::is_none")]
363 description: Option<String>,
364 },
365 Oauth2 {
366 flows: Box<OAuthFlows>,
367 #[serde(skip_serializing_if = "Option::is_none")]
368 description: Option<String>,
369 },
370 OpenIdConnect {
371 open_id_connect_url: String,
372 #[serde(skip_serializing_if = "Option::is_none")]
373 description: Option<String>,
374 },
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize, Default)]
378#[serde(rename_all = "camelCase")]
379pub struct OAuthFlows {
380 #[serde(skip_serializing_if = "Option::is_none")]
381 pub implicit: Option<OAuthFlow>,
382 #[serde(skip_serializing_if = "Option::is_none")]
383 pub password: Option<OAuthFlow>,
384 #[serde(skip_serializing_if = "Option::is_none")]
385 pub client_credentials: Option<OAuthFlow>,
386 #[serde(skip_serializing_if = "Option::is_none")]
387 pub authorization_code: Option<OAuthFlow>,
388}
389
390#[derive(Debug, Clone, Serialize, Deserialize)]
391#[serde(rename_all = "camelCase")]
392pub struct OAuthFlow {
393 #[serde(skip_serializing_if = "Option::is_none")]
394 pub authorization_url: Option<String>,
395 #[serde(skip_serializing_if = "Option::is_none")]
396 pub token_url: Option<String>,
397 #[serde(skip_serializing_if = "Option::is_none")]
398 pub refresh_url: Option<String>,
399 pub scopes: BTreeMap<String, String>,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct Tag {
404 pub name: String,
405 #[serde(skip_serializing_if = "Option::is_none")]
406 pub description: Option<String>,
407 #[serde(skip_serializing_if = "Option::is_none")]
408 pub external_docs: Option<ExternalDocs>,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize)]
412pub struct ExternalDocs {
413 pub url: String,
414 #[serde(skip_serializing_if = "Option::is_none")]
415 pub description: Option<String>,
416}
417
418pub trait OperationModifier {
420 fn update_operation(op: &mut Operation);
421}
422
423pub trait ResponseModifier {
424 fn update_response(op: &mut Operation);
425}
426
427impl<T: OperationModifier> OperationModifier for Option<T> {
429 fn update_operation(op: &mut Operation) {
430 T::update_operation(op);
431 if let Some(body) = &mut op.request_body {
432 body.required = Some(false);
433 }
434 }
435}
436
437impl<T: OperationModifier, E> OperationModifier for Result<T, E> {
438 fn update_operation(op: &mut Operation) {
439 T::update_operation(op);
440 }
441}
442
443macro_rules! impl_op_modifier_for_primitives {
444 ($($ty:ty),*) => {
445 $(
446 impl OperationModifier for $ty {
447 fn update_operation(_op: &mut Operation) {}
448 }
449 )*
450 };
451}
452impl_op_modifier_for_primitives!(
453 i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool, String
454);
455
456impl ResponseModifier for () {
457 fn update_response(op: &mut Operation) {
458 op.responses.insert(
459 "200".to_string(),
460 ResponseSpec {
461 description: "Successful response".into(),
462 ..Default::default()
463 },
464 );
465 }
466}
467
468impl ResponseModifier for String {
469 fn update_response(op: &mut Operation) {
470 let mut content = BTreeMap::new();
471 content.insert(
472 "text/plain".to_string(),
473 MediaType {
474 schema: Some(SchemaRef::Inline(serde_json::json!({"type": "string"}))),
475 example: None,
476 },
477 );
478 op.responses.insert(
479 "200".to_string(),
480 ResponseSpec {
481 description: "Successful response".into(),
482 content,
483 ..Default::default()
484 },
485 );
486 }
487}
488
489impl ResponseModifier for &'static str {
490 fn update_response(op: &mut Operation) {
491 String::update_response(op);
492 }
493}
494
495impl<T: ResponseModifier> ResponseModifier for Option<T> {
496 fn update_response(op: &mut Operation) {
497 T::update_response(op);
498 }
499}
500
501impl<T: ResponseModifier, E: ResponseModifier> ResponseModifier for Result<T, E> {
502 fn update_response(op: &mut Operation) {
503 T::update_response(op);
504 E::update_response(op);
505 }
506}
507
508impl<T> ResponseModifier for http::Response<T> {
509 fn update_response(op: &mut Operation) {
510 op.responses.insert(
511 "200".to_string(),
512 ResponseSpec {
513 description: "Successful response".into(),
514 ..Default::default()
515 },
516 );
517 }
518}