1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum ApiPrimitiveError {
10 Empty,
12 Invalid,
14 Unknown,
16}
17
18impl fmt::Display for ApiPrimitiveError {
19 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20 match self {
21 Self::Empty => formatter.write_str("API primitive value cannot be empty"),
22 Self::Invalid => formatter.write_str("invalid API primitive value"),
23 Self::Unknown => formatter.write_str("unknown API primitive label"),
24 }
25 }
26}
27
28impl Error for ApiPrimitiveError {}
29
30fn validate_api_text(value: &str) -> Result<&str, ApiPrimitiveError> {
31 let trimmed = value.trim();
32 if trimmed.is_empty() {
33 return Err(ApiPrimitiveError::Empty);
34 }
35 if trimmed.chars().any(char::is_control) {
36 return Err(ApiPrimitiveError::Invalid);
37 }
38 Ok(trimmed)
39}
40
41macro_rules! text_newtype {
42 ($name:ident) => {
43 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
44 pub struct $name(String);
45
46 impl $name {
47 pub fn new(value: impl AsRef<str>) -> Result<Self, ApiPrimitiveError> {
53 validate_api_text(value.as_ref()).map(|value| Self(value.to_owned()))
54 }
55
56 pub fn parse(value: impl AsRef<str>) -> Result<Self, ApiPrimitiveError> {
62 Self::new(value)
63 }
64
65 #[must_use]
67 pub fn as_str(&self) -> &str {
68 &self.0
69 }
70
71 #[must_use]
73 pub fn into_string(self) -> String {
74 self.0
75 }
76 }
77
78 impl AsRef<str> for $name {
79 fn as_ref(&self) -> &str {
80 self.as_str()
81 }
82 }
83
84 impl fmt::Display for $name {
85 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
86 formatter.write_str(self.as_str())
87 }
88 }
89
90 impl FromStr for $name {
91 type Err = ApiPrimitiveError;
92
93 fn from_str(value: &str) -> Result<Self, Self::Err> {
94 Self::new(value)
95 }
96 }
97
98 impl TryFrom<&str> for $name {
99 type Error = ApiPrimitiveError;
100
101 fn try_from(value: &str) -> Result<Self, Self::Error> {
102 Self::new(value)
103 }
104 }
105 };
106}
107
108text_newtype!(OpenApiPath);
109text_newtype!(OperationId);
110text_newtype!(ParameterName);
111text_newtype!(RequestBodyName);
112text_newtype!(ResponseName);
113text_newtype!(SchemaName);
114text_newtype!(TagName);
115text_newtype!(ComponentName);
116
117#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
119pub enum OpenApiOperationKind {
120 Get,
122 Post,
124 Put,
126 Patch,
128 Delete,
130 Options,
132 Head,
134 Trace,
136}
137
138impl OpenApiOperationKind {
139 #[must_use]
141 pub const fn as_str(self) -> &'static str {
142 match self {
143 Self::Get => "get",
144 Self::Post => "post",
145 Self::Put => "put",
146 Self::Patch => "patch",
147 Self::Delete => "delete",
148 Self::Options => "options",
149 Self::Head => "head",
150 Self::Trace => "trace",
151 }
152 }
153}
154
155impl Default for OpenApiOperationKind {
156 fn default() -> Self {
157 Self::Get
158 }
159}
160
161impl fmt::Display for OpenApiOperationKind {
162 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
163 formatter.write_str(self.as_str())
164 }
165}
166
167impl FromStr for OpenApiOperationKind {
168 type Err = ApiPrimitiveError;
169
170 fn from_str(value: &str) -> Result<Self, Self::Err> {
171 let trimmed = value.trim();
172 if trimmed.is_empty() {
173 return Err(ApiPrimitiveError::Empty);
174 }
175 let normalized = trimmed.to_ascii_lowercase().replace('_', "-");
176 match normalized.as_str() {
177 "get" => Ok(Self::Get),
178 "post" => Ok(Self::Post),
179 "put" => Ok(Self::Put),
180 "patch" => Ok(Self::Patch),
181 "delete" => Ok(Self::Delete),
182 "options" => Ok(Self::Options),
183 "head" => Ok(Self::Head),
184 "trace" => Ok(Self::Trace),
185 _ => Err(ApiPrimitiveError::Unknown),
186 }
187 }
188}
189#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
191pub enum OpenApiParameterLocation {
192 Path,
194 Query,
196 Header,
198 Cookie,
200}
201
202impl OpenApiParameterLocation {
203 #[must_use]
205 pub const fn as_str(self) -> &'static str {
206 match self {
207 Self::Path => "path",
208 Self::Query => "query",
209 Self::Header => "header",
210 Self::Cookie => "cookie",
211 }
212 }
213}
214
215impl Default for OpenApiParameterLocation {
216 fn default() -> Self {
217 Self::Path
218 }
219}
220
221impl fmt::Display for OpenApiParameterLocation {
222 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
223 formatter.write_str(self.as_str())
224 }
225}
226
227impl FromStr for OpenApiParameterLocation {
228 type Err = ApiPrimitiveError;
229
230 fn from_str(value: &str) -> Result<Self, Self::Err> {
231 let trimmed = value.trim();
232 if trimmed.is_empty() {
233 return Err(ApiPrimitiveError::Empty);
234 }
235 let normalized = trimmed.to_ascii_lowercase().replace('_', "-");
236 match normalized.as_str() {
237 "path" => Ok(Self::Path),
238 "query" => Ok(Self::Query),
239 "header" => Ok(Self::Header),
240 "cookie" => Ok(Self::Cookie),
241 _ => Err(ApiPrimitiveError::Unknown),
242 }
243 }
244}
245
246#[derive(Clone, Debug, Eq, PartialEq)]
248pub struct PrimitiveMetadata {
249 name: OpenApiPath,
250 kind: OpenApiOperationKind,
251}
252
253impl PrimitiveMetadata {
254 #[must_use]
256 pub const fn new(name: OpenApiPath, kind: OpenApiOperationKind) -> Self {
257 Self { name, kind }
258 }
259
260 #[must_use]
262 pub const fn name(&self) -> &OpenApiPath {
263 &self.name
264 }
265
266 #[must_use]
268 pub const fn kind(&self) -> OpenApiOperationKind {
269 self.kind
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn parses_and_displays_text() -> Result<(), ApiPrimitiveError> {
279 let value = OpenApiPath::new("listUsers")?;
280
281 assert_eq!(value.as_str(), "listUsers");
282 assert_eq!(value.to_string(), "listUsers");
283 assert_eq!("listUsers".parse::<OpenApiPath>()?, value);
284 Ok(())
285 }
286
287 #[test]
288 fn rejects_empty_text() {
289 assert_eq!(OpenApiPath::new(""), Err(ApiPrimitiveError::Empty));
290 }
291
292 #[test]
293 fn parses_and_displays_labels() -> Result<(), ApiPrimitiveError> {
294 let kind = "get".parse::<OpenApiOperationKind>()?;
295
296 assert_eq!(kind, OpenApiOperationKind::Get);
297 assert_eq!(kind.to_string(), "get");
298 Ok(())
299 }
300
301 #[test]
302 fn creates_metadata() -> Result<(), ApiPrimitiveError> {
303 let metadata = PrimitiveMetadata::new(
304 OpenApiPath::new("listUsers")?,
305 OpenApiOperationKind::default(),
306 );
307
308 assert_eq!(metadata.name().as_str(), "listUsers");
309 assert_eq!(metadata.kind(), OpenApiOperationKind::default());
310 Ok(())
311 }
312}