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