objectstore_types/
usecase.rs1use std::borrow::Borrow;
4use std::fmt;
5use std::str::FromStr;
6
7use serde::{Deserialize, Deserializer, Serialize};
8
9pub const MIN_USECASE_NAME_LENGTH: usize = 3;
11
12pub const MAX_USECASE_NAME_LENGTH: usize = 64;
14
15#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
33#[serde(transparent)]
34pub struct Usecase(String);
35
36impl Usecase {
37 pub fn new(name: impl Into<String>) -> Result<Self, InvalidUsecaseName> {
44 let name = name.into();
45 if !(MIN_USECASE_NAME_LENGTH..=MAX_USECASE_NAME_LENGTH).contains(&name.len()) {
46 return Err(InvalidUsecaseName::InvalidLength);
47 }
48 for c in name.chars() {
49 if !c.is_ascii_alphanumeric() && c != '_' && c != '-' {
50 return Err(InvalidUsecaseName::InvalidChar(c));
51 }
52 }
53 Ok(Self(name))
54 }
55
56 pub fn as_str(&self) -> &str {
58 &self.0
59 }
60}
61
62impl AsRef<str> for Usecase {
63 fn as_ref(&self) -> &str {
64 self.as_str()
65 }
66}
67
68impl Borrow<str> for Usecase {
69 fn borrow(&self) -> &str {
70 self.as_str()
71 }
72}
73
74impl fmt::Display for Usecase {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 f.write_str(self.as_str())
77 }
78}
79
80impl FromStr for Usecase {
81 type Err = InvalidUsecaseName;
82
83 fn from_str(name: &str) -> Result<Self, Self::Err> {
84 Self::new(name)
85 }
86}
87
88impl TryFrom<&str> for Usecase {
89 type Error = InvalidUsecaseName;
90
91 fn try_from(name: &str) -> Result<Self, Self::Error> {
92 Self::new(name)
93 }
94}
95
96impl TryFrom<String> for Usecase {
97 type Error = InvalidUsecaseName;
98
99 fn try_from(name: String) -> Result<Self, Self::Error> {
100 Self::new(name)
101 }
102}
103
104impl From<Usecase> for String {
105 fn from(name: Usecase) -> Self {
106 name.0
107 }
108}
109
110impl<'de> Deserialize<'de> for Usecase {
111 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
112 where
113 D: Deserializer<'de>,
114 {
115 Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
116 }
117}
118
119#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
121pub enum InvalidUsecaseName {
122 #[error("usecase name must be 3–64 ASCII characters long")]
124 InvalidLength,
125 #[error("invalid character {0:?} in usecase name; expected ASCII letters, digits, '_' or '-'")]
127 InvalidChar(char),
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn validates_names() {
136 for name in [
137 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-",
138 "___",
139 ] {
140 assert_eq!(Usecase::new(name).unwrap().as_str(), name);
141 }
142 for name in ["", "_", "ab", &"a".repeat(65)] {
143 assert_eq!(Usecase::new(name), Err(InvalidUsecaseName::InvalidLength));
144 }
145 for (name, character) in [
146 ("testing/org=999/injected", '/'),
147 ("test.name", '.'),
148 ("test name", ' '),
149 ("café", 'é'),
150 ] {
151 assert_eq!(
152 Usecase::new(name),
153 Err(InvalidUsecaseName::InvalidChar(character))
154 );
155 }
156 }
157
158 #[test]
159 fn serde_validates_and_uses_strings() {
160 let name = Usecase::new("Debug_Files-123").unwrap();
161 let serialized = serde_json::to_string(&name).unwrap();
162 assert_eq!(serialized, r#""Debug_Files-123""#);
163 assert_eq!(serde_json::from_str::<Usecase>(&serialized).unwrap(), name);
164 assert!(serde_json::from_str::<Usecase>(r#""_""#).is_err());
165 }
166
167 #[test]
168 fn string_conversions_preserve_names() {
169 let input = "Debug_Files-123";
170 let name: Usecase = input.parse().unwrap();
171 assert_eq!(name.as_ref(), input);
172 assert_eq!(name.to_string(), input);
173 let owned = String::from(name);
174 assert_eq!(Usecase::try_from(owned).unwrap().as_str(), input);
175 }
176}