asimov_core/
module_name.rs1use alloc::{borrow::ToOwned, string::String};
4use core::{borrow::Borrow, fmt, ops::Deref, str::FromStr};
5
6#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
16pub struct ModuleName(String);
17
18impl ModuleName {
19 pub const MAX_LENGTH: usize = 64;
20
21 pub fn as_str(&self) -> &str {
22 &self.0
23 }
24
25 pub fn into_string(self) -> String {
26 self.0
27 }
28}
29
30impl TryFrom<String> for ModuleName {
31 type Error = InvalidModuleName;
32
33 fn try_from(name: String) -> Result<Self, Self::Error> {
34 let valid = name.len() <= Self::MAX_LENGTH
35 && name.starts_with(|c: char| c.is_ascii_lowercase())
36 && !name.ends_with('-')
37 && name
38 .bytes()
39 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-');
40
41 if valid {
42 Ok(Self(name))
43 } else {
44 Err(InvalidModuleName(name))
45 }
46 }
47}
48
49impl TryFrom<&str> for ModuleName {
50 type Error = InvalidModuleName;
51
52 fn try_from(name: &str) -> Result<Self, Self::Error> {
53 name.to_owned().try_into()
54 }
55}
56
57impl FromStr for ModuleName {
58 type Err = InvalidModuleName;
59
60 fn from_str(name: &str) -> Result<Self, Self::Err> {
61 name.try_into()
62 }
63}
64
65impl From<ModuleName> for String {
66 fn from(name: ModuleName) -> Self {
67 name.0
68 }
69}
70
71impl AsRef<str> for ModuleName {
72 fn as_ref(&self) -> &str {
73 &self.0
74 }
75}
76
77impl Borrow<str> for ModuleName {
78 fn borrow(&self) -> &str {
79 &self.0
80 }
81}
82
83impl Deref for ModuleName {
84 type Target = str;
85
86 fn deref(&self) -> &Self::Target {
87 &self.0
88 }
89}
90
91impl fmt::Display for ModuleName {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 f.write_str(&self.0)
94 }
95}
96
97#[derive(Clone, Debug, PartialEq, Eq)]
98pub struct InvalidModuleName(pub String);
99
100impl fmt::Display for InvalidModuleName {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 write!(f, "invalid module name `{}`", self.0)
103 }
104}
105
106impl core::error::Error for InvalidModuleName {}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn accepts_module_names() {
114 for name in [
115 "ipfs",
116 "search-google-fetcher",
117 "x",
118 "a1-b2",
119 &"a".repeat(ModuleName::MAX_LENGTH),
120 ] {
121 assert!(name.parse::<ModuleName>().is_ok(), "{name}");
122 }
123 }
124
125 #[test]
126 fn rejects_invalid_names() {
127 for name in [
128 "",
129 "0-leading-digit",
130 "-sample",
131 "Sample",
132 "sample name",
133 "sample_name",
134 "sample-",
135 &"a".repeat(ModuleName::MAX_LENGTH + 1),
136 ] {
137 assert!(name.parse::<ModuleName>().is_err(), "{name}");
138 }
139 }
140
141 #[test]
142 fn rejects_names_that_are_not_path_components() {
143 for name in [
144 "..",
145 "../victim",
146 "sample/../../victim",
147 "a/b",
148 "sample.json",
149 ] {
150 assert!(name.parse::<ModuleName>().is_err(), "{name}");
151 }
152 }
153}