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, Default)]
11#[serde(rename_all = "camelCase")]
12pub struct OpenApiSpec {
13    /// OpenAPI version (always "3.1.0")
14    #[serde(default = "default_openapi_version")]
15    pub openapi: String,
16
17    /// API information
18    #[serde(default)]
19    pub info: ApiInfo,
20
21    /// JSON Schema dialect (optional, defaults to JSON Schema 2020-12)
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub json_schema_dialect: Option<String>,
24
25    /// Server list
26    #[serde(default, skip_serializing_if = "Vec::is_empty")]
27    pub servers: Vec<Server>,
28
29    /// API paths
30    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
31    pub paths: BTreeMap<String, PathItem>,
32
33    /// Webhooks
34    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
35    pub webhooks: BTreeMap<String, PathItem>,
36
37    /// Components
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub components: Option<Components>,
40
41    /// Security requirements
42    #[serde(default, skip_serializing_if = "Vec::is_empty")]
43    pub security: Vec<BTreeMap<String, Vec<String>>>,
44
45    /// Tags
46    #[serde(default, skip_serializing_if = "Vec::is_empty")]
47    pub tags: Vec<Tag>,
48
49    /// External documentation
50    #[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    /// Register a type that implements RustApiSchema
105    pub fn register<T: crate::schema::RustApiSchema>(mut self) -> Self {
106        self.register_in_place::<T>();
107        self
108    }
109
110    /// Register a type into this spec in-place.
111    pub fn register_in_place<T: crate::schema::RustApiSchema>(&mut self) {
112        let mut ctx = crate::schema::SchemaCtx::new();
113
114        // Pre-load existing schemas to avoid re-generating or to handle deduplication correctly
115        if let Some(c) = &self.components {
116            ctx.components = c.schemas.clone();
117        }
118
119        // Generate schema for T (and dependencies)
120        let _ = T::schema(&mut ctx);
121
122        // Merge back into components
123        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    /// Validate that all $ref references point to existing components.
154    /// Returns Ok(()) if valid, or a list of missing references.
155    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        // Helper to check a single ref
166        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            // Ignore other refs for now (e.g. external or non-schema refs)
171        };
172
173        // 1. Visit Paths
174        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        // 2. Visit Webhooks
179        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        // 3. Visit Components
184        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            // Deduplicate
211            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) = &param.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(_) => {} // Inline JSON value, assume safe or valid
323    }
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    /// MCP tool metadata (serialized as OpenAPI extension `x-mcp`).
481    #[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    /// Attach MCP-specific metadata to this operation (serialized as `x-mcp` extension).
504    pub fn mcp(mut self, meta: McpOperation) -> Self {
505        self.x_mcp = Some(meta);
506        self
507    }
508}
509
510/// MCP-specific metadata for an operation.
511/// Serialized under the `x-mcp` extension in OpenAPI so that MCP tools can
512/// carry permission and confirmation hints.
513#[derive(Debug, Clone, Serialize, Deserialize, Default)]
514pub struct McpOperation {
515    /// Skip exposing this operation as an MCP tool entirely.
516    #[serde(skip_serializing_if = "Option::is_none")]
517    pub skip: Option<bool>,
518
519    /// Force this operation to be treated as read-only (even if the HTTP method is POST etc.).
520    #[serde(skip_serializing_if = "Option::is_none")]
521    pub readonly: Option<bool>,
522
523    /// Explicitly mark as a write operation.
524    #[serde(skip_serializing_if = "Option::is_none")]
525    pub write: Option<bool>,
526
527    /// Agent should ask for confirmation before calling. The value can be "confirm"
528    /// or a human message.
529    #[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
675// Re-exports/Traits needed for backwards compatibility or easy migration
676pub 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
688// Helper implementations for OperationModifier/ResponseModifier
689impl<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}