1use serde::{Deserialize, Serialize};
4use std::collections::{BTreeMap, HashSet};
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(
62 "https://spec.openapis.org/oas/3.1/dialect/base".to_string(),
63 ),
64 servers: Vec::new(),
65 paths: BTreeMap::new(),
66 webhooks: BTreeMap::new(),
67 components: None,
68 security: Vec::new(),
69 tags: Vec::new(),
70 external_docs: None,
71 }
72 }
73
74 pub fn description(mut self, desc: impl Into<String>) -> Self {
75 self.info.description = Some(desc.into());
76 self
77 }
78
79 pub fn summary(mut self, summary: impl Into<String>) -> Self {
80 self.info.summary = Some(summary.into());
81 self
82 }
83
84 pub fn path(mut self, path: &str, method: &str, operation: Operation) -> Self {
85 let item = self.paths.entry(path.to_string()).or_default();
86 match method.to_uppercase().as_str() {
87 "GET" => item.get = Some(operation),
88 "POST" => item.post = Some(operation),
89 "PUT" => item.put = Some(operation),
90 "PATCH" => item.patch = Some(operation),
91 "DELETE" => item.delete = Some(operation),
92 "HEAD" => item.head = Some(operation),
93 "OPTIONS" => item.options = Some(operation),
94 "TRACE" => item.trace = Some(operation),
95 _ => {}
96 }
97 self
98 }
99
100 pub fn register<T: crate::schema::RustApiSchema>(mut self) -> Self {
102 self.register_in_place::<T>();
103 self
104 }
105
106 pub fn register_in_place<T: crate::schema::RustApiSchema>(&mut self) {
108 let mut ctx = crate::schema::SchemaCtx::new();
109
110 if let Some(c) = &self.components {
112 ctx.components = c.schemas.clone();
113 }
114
115 let _ = T::schema(&mut ctx);
117
118 let components = self.components.get_or_insert_with(Components::default);
120 for (name, schema) in ctx.components {
121 if let Some(existing) = components.schemas.get(&name) {
122 if existing != &schema {
123 panic!("Schema collision detected for component '{}'. Existing schema differs from new schema. This usually means two different types are mapped to the same component name. Please implement `RustApiSchema::name()` or alias the type.", name);
124 }
125 } else {
126 components.schemas.insert(name, schema);
127 }
128 }
129 }
130
131 pub fn server(mut self, server: Server) -> Self {
132 self.servers.push(server);
133 self
134 }
135
136 pub fn security_scheme(mut self, name: impl Into<String>, scheme: SecurityScheme) -> Self {
137 let components = self.components.get_or_insert_with(Components::default);
138 components
139 .security_schemes
140 .entry(name.into())
141 .or_insert(scheme);
142 self
143 }
144
145 pub fn to_json(&self) -> serde_json::Value {
146 serde_json::to_value(self).unwrap_or(serde_json::Value::Null)
147 }
148
149 pub fn validate_integrity(&self) -> Result<(), Vec<String>> {
152 let mut defined_schemas = HashSet::new();
153 if let Some(components) = &self.components {
154 for key in components.schemas.keys() {
155 defined_schemas.insert(format!("#/components/schemas/{}", key));
156 }
157 }
158
159 let mut missing_refs = Vec::new();
160
161 let mut check_ref = |r: &str| {
163 if r.starts_with("#/components/schemas/") && !defined_schemas.contains(r) {
164 missing_refs.push(r.to_string());
165 }
166 };
168
169 for path_item in self.paths.values() {
171 visit_path_item(path_item, &mut |s| visit_schema_ref(s, &mut check_ref));
172 }
173
174 for path_item in self.webhooks.values() {
176 visit_path_item(path_item, &mut |s| visit_schema_ref(s, &mut check_ref));
177 }
178
179 if let Some(components) = &self.components {
181 for schema in components.schemas.values() {
182 visit_json_schema(schema, &mut check_ref);
183 }
184 for resp in components.responses.values() {
185 visit_response(resp, &mut |s| visit_schema_ref(s, &mut check_ref));
186 }
187 for param in components.parameters.values() {
188 visit_parameter(param, &mut |s| visit_schema_ref(s, &mut check_ref));
189 }
190 for body in components.request_bodies.values() {
191 visit_request_body(body, &mut |s| visit_schema_ref(s, &mut check_ref));
192 }
193 for header in components.headers.values() {
194 visit_header(header, &mut |s| visit_schema_ref(s, &mut check_ref));
195 }
196 for callback_map in components.callbacks.values() {
197 for item in callback_map.values() {
198 visit_path_item(item, &mut |s| visit_schema_ref(s, &mut check_ref));
199 }
200 }
201 }
202
203 if missing_refs.is_empty() {
204 Ok(())
205 } else {
206 missing_refs.sort();
208 missing_refs.dedup();
209 Err(missing_refs)
210 }
211 }
212}
213
214fn visit_path_item<F>(item: &PathItem, visit: &mut F)
215where
216 F: FnMut(&SchemaRef),
217{
218 if let Some(op) = &item.get {
219 visit_operation(op, visit);
220 }
221 if let Some(op) = &item.put {
222 visit_operation(op, visit);
223 }
224 if let Some(op) = &item.post {
225 visit_operation(op, visit);
226 }
227 if let Some(op) = &item.delete {
228 visit_operation(op, visit);
229 }
230 if let Some(op) = &item.options {
231 visit_operation(op, visit);
232 }
233 if let Some(op) = &item.head {
234 visit_operation(op, visit);
235 }
236 if let Some(op) = &item.patch {
237 visit_operation(op, visit);
238 }
239 if let Some(op) = &item.trace {
240 visit_operation(op, visit);
241 }
242
243 for param in &item.parameters {
244 visit_parameter(param, visit);
245 }
246}
247
248fn visit_operation<F>(op: &Operation, visit: &mut F)
249where
250 F: FnMut(&SchemaRef),
251{
252 for param in &op.parameters {
253 visit_parameter(param, visit);
254 }
255 if let Some(body) = &op.request_body {
256 visit_request_body(body, visit);
257 }
258 for resp in op.responses.values() {
259 visit_response(resp, visit);
260 }
261}
262
263fn visit_parameter<F>(param: &Parameter, visit: &mut F)
264where
265 F: FnMut(&SchemaRef),
266{
267 if let Some(s) = ¶m.schema {
268 visit(s);
269 }
270}
271
272fn visit_response<F>(resp: &ResponseSpec, visit: &mut F)
273where
274 F: FnMut(&SchemaRef),
275{
276 for media in resp.content.values() {
277 visit_media_type(media, visit);
278 }
279 for header in resp.headers.values() {
280 visit_header(header, visit);
281 }
282}
283
284fn visit_request_body<F>(body: &RequestBody, visit: &mut F)
285where
286 F: FnMut(&SchemaRef),
287{
288 for media in body.content.values() {
289 visit_media_type(media, visit);
290 }
291}
292
293fn visit_header<F>(header: &Header, visit: &mut F)
294where
295 F: FnMut(&SchemaRef),
296{
297 if let Some(s) = &header.schema {
298 visit(s);
299 }
300}
301
302fn visit_media_type<F>(media: &MediaType, visit: &mut F)
303where
304 F: FnMut(&SchemaRef),
305{
306 if let Some(s) = &media.schema {
307 visit(s);
308 }
309}
310
311fn visit_schema_ref<F>(s: &SchemaRef, check: &mut F)
312where
313 F: FnMut(&str),
314{
315 match s {
316 SchemaRef::Ref { reference } => check(reference),
317 SchemaRef::Schema(boxed) => visit_json_schema(boxed, check),
318 SchemaRef::Inline(_) => {} }
320}
321
322fn visit_json_schema<F>(s: &JsonSchema2020, check: &mut F)
323where
324 F: FnMut(&str),
325{
326 if let Some(r) = &s.reference {
327 check(r);
328 }
329 if let Some(items) = &s.items {
330 visit_json_schema(items, check);
331 }
332 if let Some(props) = &s.properties {
333 for p in props.values() {
334 visit_json_schema(p, check);
335 }
336 }
337 if let Some(crate::schema::AdditionalProperties::Schema(p)) =
338 &s.additional_properties.as_deref()
339 {
340 visit_json_schema(p, check);
341 }
342 if let Some(one_of) = &s.one_of {
343 for p in one_of {
344 visit_json_schema(p, check);
345 }
346 }
347 if let Some(any_of) = &s.any_of {
348 for p in any_of {
349 visit_json_schema(p, check);
350 }
351 }
352 if let Some(all_of) = &s.all_of {
353 for p in all_of {
354 visit_json_schema(p, check);
355 }
356 }
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize, Default)]
360#[serde(rename_all = "camelCase")]
361pub struct ApiInfo {
362 pub title: String,
363 pub version: String,
364 #[serde(skip_serializing_if = "Option::is_none")]
365 pub summary: Option<String>,
366 #[serde(skip_serializing_if = "Option::is_none")]
367 pub description: Option<String>,
368 #[serde(skip_serializing_if = "Option::is_none")]
369 pub terms_of_service: Option<String>,
370 #[serde(skip_serializing_if = "Option::is_none")]
371 pub contact: Option<Contact>,
372 #[serde(skip_serializing_if = "Option::is_none")]
373 pub license: Option<License>,
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize, Default)]
377pub struct Contact {
378 #[serde(skip_serializing_if = "Option::is_none")]
379 pub name: Option<String>,
380 #[serde(skip_serializing_if = "Option::is_none")]
381 pub url: Option<String>,
382 #[serde(skip_serializing_if = "Option::is_none")]
383 pub email: Option<String>,
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize, Default)]
387pub struct License {
388 pub name: String,
389 #[serde(skip_serializing_if = "Option::is_none")]
390 pub identifier: Option<String>,
391 #[serde(skip_serializing_if = "Option::is_none")]
392 pub url: Option<String>,
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct Server {
397 pub url: String,
398 #[serde(skip_serializing_if = "Option::is_none")]
399 pub description: Option<String>,
400 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
401 pub variables: BTreeMap<String, ServerVariable>,
402}
403
404impl Server {
405 pub fn new(url: impl Into<String>) -> Self {
406 Self {
407 url: url.into(),
408 description: None,
409 variables: BTreeMap::new(),
410 }
411 }
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
415#[serde(rename_all = "camelCase")]
416pub struct ServerVariable {
417 #[serde(rename = "enum", skip_serializing_if = "Vec::is_empty")]
418 pub enum_values: Vec<String>,
419 pub default: String,
420 #[serde(skip_serializing_if = "Option::is_none")]
421 pub description: Option<String>,
422}
423
424#[derive(Debug, Clone, Serialize, Deserialize, Default)]
425pub struct PathItem {
426 #[serde(skip_serializing_if = "Option::is_none")]
427 pub summary: Option<String>,
428 #[serde(skip_serializing_if = "Option::is_none")]
429 pub description: Option<String>,
430 #[serde(skip_serializing_if = "Option::is_none")]
431 pub get: Option<Operation>,
432 #[serde(skip_serializing_if = "Option::is_none")]
433 pub put: Option<Operation>,
434 #[serde(skip_serializing_if = "Option::is_none")]
435 pub post: Option<Operation>,
436 #[serde(skip_serializing_if = "Option::is_none")]
437 pub delete: Option<Operation>,
438 #[serde(skip_serializing_if = "Option::is_none")]
439 pub options: Option<Operation>,
440 #[serde(skip_serializing_if = "Option::is_none")]
441 pub head: Option<Operation>,
442 #[serde(skip_serializing_if = "Option::is_none")]
443 pub patch: Option<Operation>,
444 #[serde(skip_serializing_if = "Option::is_none")]
445 pub trace: Option<Operation>,
446 #[serde(skip_serializing_if = "Vec::is_empty")]
447 pub servers: Vec<Server>,
448 #[serde(skip_serializing_if = "Vec::is_empty")]
449 pub parameters: Vec<Parameter>,
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize, Default)]
453#[serde(rename_all = "camelCase")]
454pub struct Operation {
455 #[serde(skip_serializing_if = "Vec::is_empty")]
456 pub tags: Vec<String>,
457 #[serde(skip_serializing_if = "Option::is_none")]
458 pub summary: Option<String>,
459 #[serde(skip_serializing_if = "Option::is_none")]
460 pub description: Option<String>,
461 #[serde(skip_serializing_if = "Option::is_none")]
462 pub external_docs: Option<ExternalDocs>,
463 #[serde(skip_serializing_if = "Option::is_none")]
464 pub operation_id: Option<String>,
465 #[serde(skip_serializing_if = "Vec::is_empty")]
466 pub parameters: Vec<Parameter>,
467 #[serde(skip_serializing_if = "Option::is_none")]
468 pub request_body: Option<RequestBody>,
469 pub responses: BTreeMap<String, ResponseSpec>,
470 #[serde(skip_serializing_if = "Vec::is_empty")]
471 pub security: Vec<BTreeMap<String, Vec<String>>>,
472 #[serde(skip_serializing_if = "Option::is_none")]
473 pub deprecated: Option<bool>,
474}
475
476impl Operation {
477 pub fn new() -> Self {
478 Self {
479 responses: BTreeMap::from([("200".to_string(), ResponseSpec::default())]),
480 ..Default::default()
481 }
482 }
483
484 pub fn summary(mut self, s: impl Into<String>) -> Self {
485 self.summary = Some(s.into());
486 self
487 }
488
489 pub fn description(mut self, d: impl Into<String>) -> Self {
490 self.description = Some(d.into());
491 self
492 }
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize)]
496pub struct Parameter {
497 pub name: String,
498 #[serde(rename = "in")]
499 pub location: String,
500 #[serde(skip_serializing_if = "Option::is_none")]
501 pub description: Option<String>,
502 pub required: bool,
503 #[serde(skip_serializing_if = "Option::is_none")]
504 pub deprecated: Option<bool>,
505 #[serde(skip_serializing_if = "Option::is_none")]
506 pub schema: Option<SchemaRef>,
507}
508
509#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct RequestBody {
511 #[serde(skip_serializing_if = "Option::is_none")]
512 pub description: Option<String>,
513 pub content: BTreeMap<String, MediaType>,
514 #[serde(skip_serializing_if = "Option::is_none")]
515 pub required: Option<bool>,
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize, Default)]
519pub struct ResponseSpec {
520 pub description: String,
521 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
522 pub content: BTreeMap<String, MediaType>,
523 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
524 pub headers: BTreeMap<String, Header>,
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize)]
528pub struct MediaType {
529 #[serde(skip_serializing_if = "Option::is_none")]
530 pub schema: Option<SchemaRef>,
531 #[serde(skip_serializing_if = "Option::is_none")]
532 pub example: Option<serde_json::Value>,
533}
534
535#[derive(Debug, Clone, Serialize, Deserialize)]
536pub struct Header {
537 #[serde(skip_serializing_if = "Option::is_none")]
538 pub description: Option<String>,
539 #[serde(skip_serializing_if = "Option::is_none")]
540 pub schema: Option<SchemaRef>,
541}
542
543#[derive(Debug, Clone, Serialize, Deserialize, Default)]
544#[serde(rename_all = "camelCase")]
545pub struct Components {
546 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
547 pub schemas: BTreeMap<String, JsonSchema2020>,
548 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
549 pub responses: BTreeMap<String, ResponseSpec>,
550 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
551 pub parameters: BTreeMap<String, Parameter>,
552 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
553 pub examples: BTreeMap<String, serde_json::Value>,
554 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
555 pub request_bodies: BTreeMap<String, RequestBody>,
556 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
557 pub headers: BTreeMap<String, Header>,
558 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
559 pub security_schemes: BTreeMap<String, SecurityScheme>,
560 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
561 pub links: BTreeMap<String, serde_json::Value>,
562 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
563 pub callbacks: BTreeMap<String, BTreeMap<String, PathItem>>,
564}
565
566#[derive(Debug, Clone, Serialize, Deserialize)]
567#[serde(tag = "type", rename_all = "camelCase")]
568pub enum SecurityScheme {
569 ApiKey {
570 name: String,
571 #[serde(rename = "in")]
572 location: String,
573 #[serde(skip_serializing_if = "Option::is_none")]
574 description: Option<String>,
575 },
576 Http {
577 scheme: String,
578 #[serde(skip_serializing_if = "Option::is_none")]
579 bearer_format: Option<String>,
580 #[serde(skip_serializing_if = "Option::is_none")]
581 description: Option<String>,
582 },
583 Oauth2 {
584 flows: Box<OAuthFlows>,
585 #[serde(skip_serializing_if = "Option::is_none")]
586 description: Option<String>,
587 },
588 OpenIdConnect {
589 open_id_connect_url: String,
590 #[serde(skip_serializing_if = "Option::is_none")]
591 description: Option<String>,
592 },
593}
594
595#[derive(Debug, Clone, Serialize, Deserialize, Default)]
596#[serde(rename_all = "camelCase")]
597pub struct OAuthFlows {
598 #[serde(skip_serializing_if = "Option::is_none")]
599 pub implicit: Option<OAuthFlow>,
600 #[serde(skip_serializing_if = "Option::is_none")]
601 pub password: Option<OAuthFlow>,
602 #[serde(skip_serializing_if = "Option::is_none")]
603 pub client_credentials: Option<OAuthFlow>,
604 #[serde(skip_serializing_if = "Option::is_none")]
605 pub authorization_code: Option<OAuthFlow>,
606}
607
608#[derive(Debug, Clone, Serialize, Deserialize)]
609#[serde(rename_all = "camelCase")]
610pub struct OAuthFlow {
611 #[serde(skip_serializing_if = "Option::is_none")]
612 pub authorization_url: Option<String>,
613 #[serde(skip_serializing_if = "Option::is_none")]
614 pub token_url: Option<String>,
615 #[serde(skip_serializing_if = "Option::is_none")]
616 pub refresh_url: Option<String>,
617 pub scopes: BTreeMap<String, String>,
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize)]
621pub struct Tag {
622 pub name: String,
623 #[serde(skip_serializing_if = "Option::is_none")]
624 pub description: Option<String>,
625 #[serde(skip_serializing_if = "Option::is_none")]
626 pub external_docs: Option<ExternalDocs>,
627}
628
629#[derive(Debug, Clone, Serialize, Deserialize)]
630pub struct ExternalDocs {
631 pub url: String,
632 #[serde(skip_serializing_if = "Option::is_none")]
633 pub description: Option<String>,
634}
635
636pub trait OperationModifier {
638 fn update_operation(op: &mut Operation);
639
640 fn register_components(_spec: &mut OpenApiSpec) {}
641}
642
643pub trait ResponseModifier {
644 fn update_response(op: &mut Operation);
645
646 fn register_components(_spec: &mut OpenApiSpec) {}
647}
648
649impl<T: OperationModifier> OperationModifier for Option<T> {
651 fn update_operation(op: &mut Operation) {
652 T::update_operation(op);
653 if let Some(body) = &mut op.request_body {
654 body.required = Some(false);
655 }
656 }
657
658 fn register_components(spec: &mut OpenApiSpec) {
659 T::register_components(spec);
660 }
661}
662
663impl<T: OperationModifier, E> OperationModifier for Result<T, E> {
664 fn update_operation(op: &mut Operation) {
665 T::update_operation(op);
666 }
667
668 fn register_components(spec: &mut OpenApiSpec) {
669 T::register_components(spec);
670 }
671}
672
673macro_rules! impl_op_modifier_for_primitives {
674 ($($ty:ty),*) => {
675 $(
676 impl OperationModifier for $ty {
677 fn update_operation(_op: &mut Operation) {}
678 }
679 )*
680 };
681}
682impl_op_modifier_for_primitives!(
683 i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64, bool, String
684);
685
686impl ResponseModifier for () {
687 fn update_response(op: &mut Operation) {
688 op.responses.insert(
689 "200".to_string(),
690 ResponseSpec {
691 description: "Successful response".into(),
692 ..Default::default()
693 },
694 );
695 }
696}
697
698impl ResponseModifier for String {
699 fn update_response(op: &mut Operation) {
700 let mut content = BTreeMap::new();
701 content.insert(
702 "text/plain".to_string(),
703 MediaType {
704 schema: Some(SchemaRef::Inline(serde_json::json!({"type": "string"}))),
705 example: None,
706 },
707 );
708 op.responses.insert(
709 "200".to_string(),
710 ResponseSpec {
711 description: "Successful response".into(),
712 content,
713 ..Default::default()
714 },
715 );
716 }
717}
718
719impl ResponseModifier for &'static str {
720 fn update_response(op: &mut Operation) {
721 String::update_response(op);
722 }
723}
724
725impl<T: ResponseModifier> ResponseModifier for Option<T> {
726 fn update_response(op: &mut Operation) {
727 T::update_response(op);
728 }
729
730 fn register_components(spec: &mut OpenApiSpec) {
731 T::register_components(spec);
732 }
733}
734
735impl<T: ResponseModifier, E: ResponseModifier> ResponseModifier for Result<T, E> {
736 fn update_response(op: &mut Operation) {
737 T::update_response(op);
738 E::update_response(op);
739 }
740
741 fn register_components(spec: &mut OpenApiSpec) {
742 T::register_components(spec);
743 E::register_components(spec);
744 }
745}
746
747impl<T> ResponseModifier for http::Response<T> {
748 fn update_response(op: &mut Operation) {
749 op.responses.insert(
750 "200".to_string(),
751 ResponseSpec {
752 description: "Successful response".into(),
753 ..Default::default()
754 },
755 );
756 }
757}