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!(ResourceName);
109text_newtype!(ResourceIdentifier);
110text_newtype!(CollectionName);
111text_newtype!(RepresentationName);
112
113#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
115pub enum RestAction {
116 List,
118 Retrieve,
120 Create,
122 Update,
124 Delete,
126 Patch,
128 Search,
130}
131
132impl RestAction {
133 #[must_use]
135 pub const fn as_str(self) -> &'static str {
136 match self {
137 Self::List => "list",
138 Self::Retrieve => "retrieve",
139 Self::Create => "create",
140 Self::Update => "update",
141 Self::Delete => "delete",
142 Self::Patch => "patch",
143 Self::Search => "search",
144 }
145 }
146}
147
148impl Default for RestAction {
149 fn default() -> Self {
150 Self::List
151 }
152}
153
154impl fmt::Display for RestAction {
155 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
156 formatter.write_str(self.as_str())
157 }
158}
159
160impl FromStr for RestAction {
161 type Err = ApiPrimitiveError;
162
163 fn from_str(value: &str) -> Result<Self, Self::Err> {
164 let trimmed = value.trim();
165 if trimmed.is_empty() {
166 return Err(ApiPrimitiveError::Empty);
167 }
168 let normalized = trimmed.to_ascii_lowercase().replace('_', "-");
169 match normalized.as_str() {
170 "list" => Ok(Self::List),
171 "retrieve" => Ok(Self::Retrieve),
172 "create" => Ok(Self::Create),
173 "update" => Ok(Self::Update),
174 "delete" => Ok(Self::Delete),
175 "patch" => Ok(Self::Patch),
176 "search" => Ok(Self::Search),
177 _ => Err(ApiPrimitiveError::Unknown),
178 }
179 }
180}
181#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
183pub enum RestConstraint {
184 Stateless,
186 Cacheable,
188 UniformInterface,
190 LayeredSystem,
192}
193
194impl RestConstraint {
195 #[must_use]
197 pub const fn as_str(self) -> &'static str {
198 match self {
199 Self::Stateless => "stateless",
200 Self::Cacheable => "cacheable",
201 Self::UniformInterface => "uniform-interface",
202 Self::LayeredSystem => "layered-system",
203 }
204 }
205}
206
207impl Default for RestConstraint {
208 fn default() -> Self {
209 Self::Stateless
210 }
211}
212
213impl fmt::Display for RestConstraint {
214 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
215 formatter.write_str(self.as_str())
216 }
217}
218
219impl FromStr for RestConstraint {
220 type Err = ApiPrimitiveError;
221
222 fn from_str(value: &str) -> Result<Self, Self::Err> {
223 let trimmed = value.trim();
224 if trimmed.is_empty() {
225 return Err(ApiPrimitiveError::Empty);
226 }
227 let normalized = trimmed.to_ascii_lowercase().replace('_', "-");
228 match normalized.as_str() {
229 "stateless" => Ok(Self::Stateless),
230 "cacheable" => Ok(Self::Cacheable),
231 "uniform-interface" => Ok(Self::UniformInterface),
232 "layered-system" => Ok(Self::LayeredSystem),
233 _ => Err(ApiPrimitiveError::Unknown),
234 }
235 }
236}
237#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
239pub enum RepresentationKind {
240 Json,
242 Xml,
244 Form,
246 Binary,
248 Text,
250}
251
252impl RepresentationKind {
253 #[must_use]
255 pub const fn as_str(self) -> &'static str {
256 match self {
257 Self::Json => "json",
258 Self::Xml => "xml",
259 Self::Form => "form",
260 Self::Binary => "binary",
261 Self::Text => "text",
262 }
263 }
264}
265
266impl Default for RepresentationKind {
267 fn default() -> Self {
268 Self::Json
269 }
270}
271
272impl fmt::Display for RepresentationKind {
273 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
274 formatter.write_str(self.as_str())
275 }
276}
277
278impl FromStr for RepresentationKind {
279 type Err = ApiPrimitiveError;
280
281 fn from_str(value: &str) -> Result<Self, Self::Err> {
282 let trimmed = value.trim();
283 if trimmed.is_empty() {
284 return Err(ApiPrimitiveError::Empty);
285 }
286 let normalized = trimmed.to_ascii_lowercase().replace('_', "-");
287 match normalized.as_str() {
288 "json" => Ok(Self::Json),
289 "xml" => Ok(Self::Xml),
290 "form" => Ok(Self::Form),
291 "binary" => Ok(Self::Binary),
292 "text" => Ok(Self::Text),
293 _ => Err(ApiPrimitiveError::Unknown),
294 }
295 }
296}
297
298#[derive(Clone, Debug, Eq, PartialEq)]
300pub struct PrimitiveMetadata {
301 name: ResourceName,
302 kind: RestAction,
303}
304
305impl PrimitiveMetadata {
306 #[must_use]
308 pub const fn new(name: ResourceName, kind: RestAction) -> Self {
309 Self { name, kind }
310 }
311
312 #[must_use]
314 pub const fn name(&self) -> &ResourceName {
315 &self.name
316 }
317
318 #[must_use]
320 pub const fn kind(&self) -> RestAction {
321 self.kind
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn parses_and_displays_text() -> Result<(), ApiPrimitiveError> {
331 let value = ResourceName::new("users")?;
332
333 assert_eq!(value.as_str(), "users");
334 assert_eq!(value.to_string(), "users");
335 assert_eq!("users".parse::<ResourceName>()?, value);
336 Ok(())
337 }
338
339 #[test]
340 fn rejects_empty_text() {
341 assert_eq!(ResourceName::new(""), Err(ApiPrimitiveError::Empty));
342 }
343
344 #[test]
345 fn parses_and_displays_labels() -> Result<(), ApiPrimitiveError> {
346 let kind = "list".parse::<RestAction>()?;
347
348 assert_eq!(kind, RestAction::List);
349 assert_eq!(kind.to_string(), "list");
350 Ok(())
351 }
352
353 #[test]
354 fn creates_metadata() -> Result<(), ApiPrimitiveError> {
355 let metadata = PrimitiveMetadata::new(ResourceName::new("users")?, RestAction::default());
356
357 assert_eq!(metadata.name().as_str(), "users");
358 assert_eq!(metadata.kind(), RestAction::default());
359 Ok(())
360 }
361}