kynos_openapi/model/
components.rs1use 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#[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#[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 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 #[must_use]
57 pub fn is_valid_char(c: char) -> bool {
58 c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_')
59 }
60
61 #[must_use]
63 pub fn is_valid(name: &str) -> bool {
64 !name.is_empty() && name.chars().all(Self::is_valid_char)
65 }
66
67 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 #[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#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
123pub struct Components {
124 #[serde(default, skip_serializing_if = "Map::is_empty")]
126 pub schemas: Map<Schema>,
127
128 #[serde(default, skip_serializing_if = "Map::is_empty")]
130 pub responses: Map<RefOr<Response>>,
131
132 #[serde(default, skip_serializing_if = "Map::is_empty")]
134 pub parameters: Map<RefOr<Parameter>>,
135
136 #[serde(default, skip_serializing_if = "Map::is_empty")]
138 pub examples: Map<RefOr<Example>>,
139
140 #[serde(
142 rename = "requestBodies",
143 default,
144 skip_serializing_if = "Map::is_empty"
145 )]
146 pub request_bodies: Map<RefOr<RequestBody>>,
147
148 #[serde(default, skip_serializing_if = "Map::is_empty")]
150 pub headers: Map<RefOr<Header>>,
151
152 #[serde(
154 rename = "securitySchemes",
155 default,
156 skip_serializing_if = "Map::is_empty"
157 )]
158 pub security_schemes: Map<RefOr<SecurityScheme>>,
159
160 #[serde(default, skip_serializing_if = "Map::is_empty")]
162 pub links: Map<RefOr<crate::model::link::Link>>,
163
164 #[serde(default, skip_serializing_if = "Map::is_empty")]
166 pub callbacks: Map<RefOr<Callback>>,
167
168 #[serde(rename = "pathItems", default, skip_serializing_if = "Map::is_empty")]
170 pub path_items: Map<PathItem>,
171
172 #[cfg(feature = "openapi32")]
176 #[serde(rename = "mediaTypes", default, skip_serializing_if = "Map::is_empty")]
177 pub media_types: Map<RefOr<MediaType>>,
178
179 #[serde(flatten)]
181 pub extensions: Extensions,
182}
183
184impl Components {
185 #[must_use]
187 pub fn new() -> Self {
188 Self::default()
189 }
190
191 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 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 #[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;