Skip to main content

asimov_core/
module_name.rs

1// This is free and unencumbered software released into the public domain.
2
3use alloc::{borrow::ToOwned, string::String};
4use core::{borrow::Borrow, fmt, ops::Deref, str::FromStr};
5
6/// The name of a module.
7///
8/// A name consists only of lowercase letters, digits and hyphens, starts with a letter, and is at
9/// most 64 characters long. A trailing hyphen is refused as well, which the specification permits
10/// but `asimov-module-kit` does not create.
11///
12/// See: <https://asimov-specs.github.io/module-manifest/#name-field>
13#[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}