Skip to main content

kynos_openapi/model/
components.rs

1//! The Components Object and its key type.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    Map,
9    model::{
10        body::RequestBody,
11        callback::Callback,
12        example::Example,
13        extensions::Extensions,
14        parameter::{Parameter, header::Header},
15        paths::item::PathItem,
16        reference::RefOr,
17        response::Response,
18        schema::Schema,
19        security::SecurityScheme,
20    },
21};
22
23#[cfg(feature = "openapi32")]
24use crate::model::body::media_type::MediaType;
25
26/// The error returned when a component key is not a legal name.
27#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
28#[error("`{0}` is not a valid component name: expected only `A-Z a-z 0-9 . - _`")]
29pub struct InvalidComponentName(pub String);
30
31/// A key under one of the [`Components`] maps.
32///
33/// The specification restricts these to `^[a-zA-Z0-9.\-_]+$`, which is narrower
34/// than most Rust type names allow — `Vec<User>` cannot be a component name, so
35/// generic types have to be mangled into one.
36#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
37#[serde(try_from = "String", into = "String")]
38pub struct ComponentName(String);
39
40impl ComponentName {
41    /// Validates and wraps a component name.
42    ///
43    /// # Errors
44    ///
45    /// Returns [`InvalidComponentName`] when `name` is empty or contains a
46    /// character outside `A-Z a-z 0-9 . - _`.
47    pub fn new(name: impl Into<String>) -> Result<Self, InvalidComponentName> {
48        let name = name.into();
49        if name.is_empty() || !name.chars().all(Self::is_valid_char) {
50            return Err(InvalidComponentName(name));
51        }
52        Ok(Self(name))
53    }
54
55    /// Whether `c` may appear in a component name.
56    #[must_use]
57    pub fn is_valid_char(c: char) -> bool {
58        c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_')
59    }
60
61    /// Whether `name` is a legal component name.
62    #[must_use]
63    pub fn is_valid(name: &str) -> bool {
64        !name.is_empty() && name.chars().all(Self::is_valid_char)
65    }
66
67    /// Rewrites `name` into a legal component name.
68    ///
69    /// Illegal characters are replaced with `_`, and runs of them collapse.
70    /// This is how a generic Rust type name becomes a component key.
71    ///
72    /// # Errors
73    ///
74    /// Returns [`InvalidComponentName`] when nothing legal survives, which
75    /// happens only for an empty input.
76    pub fn sanitized(name: &str) -> Result<Self, InvalidComponentName> {
77        let mut out = String::with_capacity(name.len());
78        let mut previous_was_underscore = false;
79        for c in name.chars() {
80            if Self::is_valid_char(c) {
81                out.push(c);
82                previous_was_underscore = false;
83            } else if !previous_was_underscore {
84                out.push('_');
85                previous_was_underscore = true;
86            }
87        }
88        let trimmed = out.trim_matches('_');
89        Self::new(if trimmed.is_empty() { &out } else { trimmed })
90    }
91
92    /// The name as a string slice.
93    #[must_use]
94    pub fn as_str(&self) -> &str {
95        &self.0
96    }
97}
98
99impl fmt::Display for ComponentName {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        f.write_str(&self.0)
102    }
103}
104
105impl TryFrom<String> for ComponentName {
106    type Error = InvalidComponentName;
107
108    fn try_from(value: String) -> Result<Self, Self::Error> {
109        Self::new(value)
110    }
111}
112
113impl From<ComponentName> for String {
114    fn from(name: ComponentName) -> Self {
115        name.0
116    }
117}
118
119/// Reusable objects referenced from elsewhere in the description.
120///
121/// Nothing here has any effect until something refers to it.
122#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
123pub struct Components {
124    /// Reusable schemas.
125    #[serde(default, skip_serializing_if = "Map::is_empty")]
126    pub schemas: Map<Schema>,
127
128    /// Reusable responses.
129    #[serde(default, skip_serializing_if = "Map::is_empty")]
130    pub responses: Map<RefOr<Response>>,
131
132    /// Reusable parameters.
133    #[serde(default, skip_serializing_if = "Map::is_empty")]
134    pub parameters: Map<RefOr<Parameter>>,
135
136    /// Reusable examples.
137    #[serde(default, skip_serializing_if = "Map::is_empty")]
138    pub examples: Map<RefOr<Example>>,
139
140    /// Reusable request bodies.
141    #[serde(
142        rename = "requestBodies",
143        default,
144        skip_serializing_if = "Map::is_empty"
145    )]
146    pub request_bodies: Map<RefOr<RequestBody>>,
147
148    /// Reusable headers.
149    #[serde(default, skip_serializing_if = "Map::is_empty")]
150    pub headers: Map<RefOr<Header>>,
151
152    /// Security schemes the API can use.
153    #[serde(
154        rename = "securitySchemes",
155        default,
156        skip_serializing_if = "Map::is_empty"
157    )]
158    pub security_schemes: Map<RefOr<SecurityScheme>>,
159
160    /// Reusable links.
161    #[serde(default, skip_serializing_if = "Map::is_empty")]
162    pub links: Map<RefOr<crate::model::link::Link>>,
163
164    /// Reusable callbacks.
165    #[serde(default, skip_serializing_if = "Map::is_empty")]
166    pub callbacks: Map<RefOr<Callback>>,
167
168    /// Reusable path items.
169    #[serde(rename = "pathItems", default, skip_serializing_if = "Map::is_empty")]
170    pub path_items: Map<PathItem>,
171
172    /// Reusable media type definitions.
173    ///
174    /// Introduced in OpenAPI 3.2.
175    #[cfg(feature = "openapi32")]
176    #[serde(rename = "mediaTypes", default, skip_serializing_if = "Map::is_empty")]
177    pub media_types: Map<RefOr<MediaType>>,
178
179    /// Specification extensions.
180    #[serde(flatten)]
181    pub extensions: Extensions,
182}
183
184impl Components {
185    /// Creates an empty set of components.
186    #[must_use]
187    pub fn new() -> Self {
188        Self::default()
189    }
190
191    /// Registers a schema and returns a `$ref` to it.
192    pub fn insert_schema(&mut self, name: &ComponentName, schema: Schema) -> Schema {
193        self.schemas.insert(name.as_str().to_owned(), schema);
194        Schema::component(name.as_str())
195    }
196
197    /// Registers a security scheme.
198    pub fn insert_security_scheme(&mut self, name: &ComponentName, scheme: SecurityScheme) {
199        self.security_schemes
200            .insert(name.as_str().to_owned(), RefOr::Item(scheme));
201    }
202
203    /// Returns `true` when nothing is registered.
204    #[must_use]
205    pub fn is_empty(&self) -> bool {
206        let empty = self.schemas.is_empty()
207            && self.responses.is_empty()
208            && self.parameters.is_empty()
209            && self.examples.is_empty()
210            && self.request_bodies.is_empty()
211            && self.headers.is_empty()
212            && self.security_schemes.is_empty()
213            && self.links.is_empty()
214            && self.callbacks.is_empty()
215            && self.path_items.is_empty()
216            && self.extensions.is_empty();
217
218        #[cfg(feature = "openapi32")]
219        let empty = empty && self.media_types.is_empty();
220
221        empty
222    }
223}
224
225#[cfg(test)]
226mod tests;