Skip to main content

rustapi_openapi/
spec.rs

1//! OpenAPI 3.1 specification types
2
3use serde::{Deserialize, Serialize};
4use std::collections::{BTreeMap, HashSet};
5
6use crate::schema::JsonSchema2020;
7pub use crate::schema::SchemaRef;
8
9/// OpenAPI 3.1.0 specification
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct OpenApiSpec {
13    /// OpenAPI version (always "3.1.0")
14    pub openapi: String,
15
16    /// API information
17    pub info: ApiInfo,
18
19    /// JSON Schema dialect (optional, defaults to JSON Schema 2020-12)
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub json_schema_dialect: Option<String>,
22
23    /// Server list
24    #[serde(skip_serializing_if = "Vec::is_empty")]
25    pub servers: Vec<Server>,
26
27    /// API paths
28    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
29    pub paths: BTreeMap<String, PathItem>,
30
31    /// Webhooks
32    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
33    pub webhooks: BTreeMap<String, PathItem>,
34
35    /// Components
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub components: Option<Components>,
38
39    /// Security requirements
40    #[serde(skip_serializing_if = "Vec::is_empty")]
41    pub security: Vec<BTreeMap<String, Vec<String>>>,
42
43    /// Tags
44    #[serde(skip_serializing_if = "Vec::is_empty")]
45    pub tags: Vec<Tag>,
46
47    /// External documentation
48    #[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    /// Register a type that implements RustApiSchema
101    pub fn register<T: crate::schema::RustApiSchema>(mut self) -> Self {
102        self.register_in_place::<T>();
103        self
104    }
105
106    /// Register a type into this spec in-place.
107    pub fn register_in_place<T: crate::schema::RustApiSchema>(&mut self) {
108        let mut ctx = crate::schema::SchemaCtx::new();
109
110        // Pre-load existing schemas to avoid re-generating or to handle deduplication correctly
111        if let Some(c) = &self.components {
112            ctx.components = c.schemas.clone();
113        }
114
115        // Generate schema for T (and dependencies)
116        let _ = T::schema(&mut ctx);
117
118        // Merge back into components
119        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    /// Validate that all $ref references point to existing components.
150    /// Returns Ok(()) if valid, or a list of missing references.
151    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        // Helper to check a single ref
162        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            // Ignore other refs for now (e.g. external or non-schema refs)
167        };
168
169        // 1. Visit Paths
170        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        // 2. Visit Webhooks
175        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        // 3. Visit Components
180        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            // Deduplicate
207            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) = &param.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(_) => {} // Inline JSON value, assume safe or valid
319    }
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
636// Re-exports/Traits needed for backwards compatibility or easy migration
637pub 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
649// Helper implementations for OperationModifier/ResponseModifier
650impl<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}