Skip to main content

objectstore_types/
usecase.rs

1//! Validated usecase names shared by clients and the server.
2
3use std::borrow::Borrow;
4use std::fmt;
5use std::str::FromStr;
6
7use serde::{Deserialize, Deserializer, Serialize};
8
9/// Minimum length of a usecase name in bytes.
10pub const MIN_USECASE_NAME_LENGTH: usize = 3;
11
12/// Maximum length of a usecase name in bytes.
13pub const MAX_USECASE_NAME_LENGTH: usize = 64;
14
15/// A validated, case-sensitive name for an Objectstore workload.
16///
17/// Names must be 3–64 ASCII characters from `[A-Za-z0-9_-]`. Any permitted
18/// character may appear in any position, including names consisting entirely of
19/// underscores or hyphens. Names are never normalized.
20///
21/// Construction and deserialization both validate the name. The contents cannot
22/// be mutated after construction.
23///
24/// ```
25/// use objectstore_types::usecase::Usecase;
26///
27/// let name = Usecase::new("debug-files")?;
28/// assert_eq!(name.as_str(), "debug-files");
29/// assert!(Usecase::new("testing/org=999/injected").is_err());
30/// # Ok::<(), objectstore_types::usecase::InvalidUsecaseName>(())
31/// ```
32#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
33#[serde(transparent)]
34pub struct Usecase(String);
35
36impl Usecase {
37    /// Creates a validated usecase name from a borrowed or owned string.
38    ///
39    /// # Errors
40    ///
41    /// Returns [`InvalidUsecaseName`] if the name violates the rules on
42    /// [`Usecase`].
43    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    /// Returns the usecase name as a string slice.
57    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/// An error indicating that a usecase name is invalid.
120#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
121pub enum InvalidUsecaseName {
122    /// The name is outside the permitted length range.
123    #[error("usecase name must be 3–64 ASCII characters long")]
124    InvalidLength,
125    /// The name contains a character outside the permitted set.
126    #[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}