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!(ServiceName);
109text_newtype!(MethodName);
110text_newtype!(GrpcMethodPath);
111text_newtype!(MetadataKey);
112
113#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
115pub enum GrpcStatusCode {
116 Ok,
118 Cancelled,
120 Unknown,
122 InvalidArgument,
124 DeadlineExceeded,
126 NotFound,
128 AlreadyExists,
130 PermissionDenied,
132 ResourceExhausted,
134 FailedPrecondition,
136 Aborted,
138 OutOfRange,
140 Unimplemented,
142 Internal,
144 Unavailable,
146 DataLoss,
148 Unauthenticated,
150}
151
152impl GrpcStatusCode {
153 #[must_use]
155 pub const fn as_str(self) -> &'static str {
156 match self {
157 Self::Ok => "ok",
158 Self::Cancelled => "cancelled",
159 Self::Unknown => "unknown",
160 Self::InvalidArgument => "invalid-argument",
161 Self::DeadlineExceeded => "deadline-exceeded",
162 Self::NotFound => "not-found",
163 Self::AlreadyExists => "already-exists",
164 Self::PermissionDenied => "permission-denied",
165 Self::ResourceExhausted => "resource-exhausted",
166 Self::FailedPrecondition => "failed-precondition",
167 Self::Aborted => "aborted",
168 Self::OutOfRange => "out-of-range",
169 Self::Unimplemented => "unimplemented",
170 Self::Internal => "internal",
171 Self::Unavailable => "unavailable",
172 Self::DataLoss => "data-loss",
173 Self::Unauthenticated => "unauthenticated",
174 }
175 }
176}
177
178impl Default for GrpcStatusCode {
179 fn default() -> Self {
180 Self::Ok
181 }
182}
183
184impl fmt::Display for GrpcStatusCode {
185 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
186 formatter.write_str(self.as_str())
187 }
188}
189
190impl FromStr for GrpcStatusCode {
191 type Err = ApiPrimitiveError;
192
193 fn from_str(value: &str) -> Result<Self, Self::Err> {
194 let trimmed = value.trim();
195 if trimmed.is_empty() {
196 return Err(ApiPrimitiveError::Empty);
197 }
198 let normalized = trimmed.to_ascii_lowercase().replace('_', "-");
199 match normalized.as_str() {
200 "ok" => Ok(Self::Ok),
201 "cancelled" => Ok(Self::Cancelled),
202 "unknown" => Ok(Self::Unknown),
203 "invalid-argument" => Ok(Self::InvalidArgument),
204 "deadline-exceeded" => Ok(Self::DeadlineExceeded),
205 "not-found" => Ok(Self::NotFound),
206 "already-exists" => Ok(Self::AlreadyExists),
207 "permission-denied" => Ok(Self::PermissionDenied),
208 "resource-exhausted" => Ok(Self::ResourceExhausted),
209 "failed-precondition" => Ok(Self::FailedPrecondition),
210 "aborted" => Ok(Self::Aborted),
211 "out-of-range" => Ok(Self::OutOfRange),
212 "unimplemented" => Ok(Self::Unimplemented),
213 "internal" => Ok(Self::Internal),
214 "unavailable" => Ok(Self::Unavailable),
215 "data-loss" => Ok(Self::DataLoss),
216 "unauthenticated" => Ok(Self::Unauthenticated),
217 _ => Err(ApiPrimitiveError::Unknown),
218 }
219 }
220}
221#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
223pub enum StreamingMode {
224 Unary,
226 ClientStreaming,
228 ServerStreaming,
230 BidirectionalStreaming,
232}
233
234impl StreamingMode {
235 #[must_use]
237 pub const fn as_str(self) -> &'static str {
238 match self {
239 Self::Unary => "unary",
240 Self::ClientStreaming => "client-streaming",
241 Self::ServerStreaming => "server-streaming",
242 Self::BidirectionalStreaming => "bidirectional-streaming",
243 }
244 }
245}
246
247impl Default for StreamingMode {
248 fn default() -> Self {
249 Self::Unary
250 }
251}
252
253impl fmt::Display for StreamingMode {
254 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
255 formatter.write_str(self.as_str())
256 }
257}
258
259impl FromStr for StreamingMode {
260 type Err = ApiPrimitiveError;
261
262 fn from_str(value: &str) -> Result<Self, Self::Err> {
263 let trimmed = value.trim();
264 if trimmed.is_empty() {
265 return Err(ApiPrimitiveError::Empty);
266 }
267 let normalized = trimmed.to_ascii_lowercase().replace('_', "-");
268 match normalized.as_str() {
269 "unary" => Ok(Self::Unary),
270 "client-streaming" => Ok(Self::ClientStreaming),
271 "server-streaming" => Ok(Self::ServerStreaming),
272 "bidirectional-streaming" => Ok(Self::BidirectionalStreaming),
273 _ => Err(ApiPrimitiveError::Unknown),
274 }
275 }
276}
277
278#[derive(Clone, Debug, Eq, PartialEq)]
280pub struct PrimitiveMetadata {
281 name: ServiceName,
282 kind: GrpcStatusCode,
283}
284
285impl PrimitiveMetadata {
286 #[must_use]
288 pub const fn new(name: ServiceName, kind: GrpcStatusCode) -> Self {
289 Self { name, kind }
290 }
291
292 #[must_use]
294 pub const fn name(&self) -> &ServiceName {
295 &self.name
296 }
297
298 #[must_use]
300 pub const fn kind(&self) -> GrpcStatusCode {
301 self.kind
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[test]
310 fn parses_and_displays_text() -> Result<(), ApiPrimitiveError> {
311 let value = ServiceName::new("/users.UserService/GetUser")?;
312
313 assert_eq!(value.as_str(), "/users.UserService/GetUser");
314 assert_eq!(value.to_string(), "/users.UserService/GetUser");
315 assert_eq!("/users.UserService/GetUser".parse::<ServiceName>()?, value);
316 Ok(())
317 }
318
319 #[test]
320 fn rejects_empty_text() {
321 assert_eq!(ServiceName::new(""), Err(ApiPrimitiveError::Empty));
322 }
323
324 #[test]
325 fn parses_and_displays_labels() -> Result<(), ApiPrimitiveError> {
326 let kind = "ok".parse::<GrpcStatusCode>()?;
327
328 assert_eq!(kind, GrpcStatusCode::Ok);
329 assert_eq!(kind.to_string(), "ok");
330 Ok(())
331 }
332
333 #[test]
334 fn creates_metadata() -> Result<(), ApiPrimitiveError> {
335 let metadata = PrimitiveMetadata::new(
336 ServiceName::new("/users.UserService/GetUser")?,
337 GrpcStatusCode::default(),
338 );
339
340 assert_eq!(metadata.name().as_str(), "/users.UserService/GetUser");
341 assert_eq!(metadata.kind(), GrpcStatusCode::default());
342 Ok(())
343 }
344}