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