libwally/
package_name.rs

1use std::fmt;
2use std::str::FromStr;
3
4use anyhow::{anyhow, ensure};
5use serde::de::{Deserialize, Deserializer, Error, Visitor};
6use serde::ser::{Serialize, Serializer};
7
8/// Refers to a package, but not a specific version. Package names consist of a
9/// scope and name.
10///
11/// Both the scope and name portions of a package name must consist only of
12/// lowercase letters, digits, and dashes (`-`).
13///
14/// Examples of package names:
15/// * `hello/world`
16/// * `osyrisrblx/t`
17#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub struct PackageName {
19    // Fields are private here to enforce invariants around what characters are
20    // valid in package names and scopes.
21    scope: String,
22    name: String,
23}
24
25impl PackageName {
26    pub fn new<S, N>(scope: S, name: N) -> anyhow::Result<Self>
27    where
28        S: Into<String>,
29        N: Into<String>,
30    {
31        let scope = scope.into();
32        let name = name.into();
33
34        validate_scope(&scope)?;
35        validate_name(&name)?;
36
37        Ok(PackageName { scope, name })
38    }
39
40    pub fn scope(&self) -> &str {
41        &self.scope
42    }
43
44    pub fn name(&self) -> &str {
45        &self.name
46    }
47}
48
49fn validate_scope(scope: &str) -> anyhow::Result<()> {
50    let only_valid_chars = scope
51        .chars()
52        .all(|char| char.is_ascii_lowercase() || char.is_ascii_digit() || char == '-');
53
54    ensure!(
55        only_valid_chars,
56        "package scope '{}' is invalid (scopes can only contain lowercase characters, digits and '-')",
57        scope
58    );
59    ensure!(scope.len() > 0, "package scopes cannot be empty");
60    ensure!(
61        scope.len() <= 64,
62        "package scopes cannot exceed 64 characters in length"
63    );
64
65    Ok(())
66}
67
68fn validate_name(name: &str) -> anyhow::Result<()> {
69    let only_valid_chars = name
70        .chars()
71        .all(|char| char.is_ascii_lowercase() || char.is_ascii_digit() || char == '-');
72
73    ensure!(
74        only_valid_chars,
75        "package name '{}' is invalid (names can only contain lowercase characters, digits and '-')",
76        name
77    );
78    ensure!(name.len() > 0, "package names cannot be empty");
79    ensure!(
80        name.len() <= 64,
81        "package names cannot exceed 64 characters in length"
82    );
83
84    Ok(())
85}
86
87impl fmt::Display for PackageName {
88    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
89        write!(formatter, "{}/{}", self.scope, self.name)
90    }
91}
92
93impl FromStr for PackageName {
94    type Err = anyhow::Error;
95
96    fn from_str(value: &str) -> anyhow::Result<Self> {
97        const WRONG_NUMBER_ERR: &str = "a package name is of the form SCOPE/NAME";
98
99        let mut pieces = value.splitn(2, '/');
100        let scope = pieces.next().ok_or_else(|| anyhow!(WRONG_NUMBER_ERR))?;
101        let name = pieces.next().ok_or_else(|| anyhow!(WRONG_NUMBER_ERR))?;
102
103        PackageName::new(scope.to_owned(), name.to_owned())
104    }
105}
106
107impl Serialize for PackageName {
108    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
109        let combined_name = format!("{}/{}", self.scope, self.name);
110        serializer.serialize_str(&combined_name)
111    }
112}
113
114impl<'de> Deserialize<'de> for PackageName {
115    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
116        deserializer.deserialize_str(PackageNameVisitor)
117    }
118}
119
120struct PackageNameVisitor;
121
122impl<'de> Visitor<'de> for PackageNameVisitor {
123    type Value = PackageName;
124
125    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
126        write!(formatter, "a package name of the form SCOPE/NAME")
127    }
128
129    fn visit_str<E: Error>(self, value: &str) -> Result<Self::Value, E> {
130        value.parse().map_err(|err| E::custom(err))
131    }
132}
133
134#[cfg(test)]
135mod test {
136    use super::*;
137
138    #[test]
139    fn new() {
140        let package = PackageName::new("flub-flab", "sisyphus-simulator-2").unwrap();
141        assert_eq!(package.scope(), "flub-flab");
142        assert_eq!(package.name(), "sisyphus-simulator-2");
143    }
144
145    #[test]
146    fn new_invalid() {
147        // Uppercase letters are not allowed.
148        assert!(PackageName::new("Upper-Skewer-Case", "Foo").is_err());
149
150        // Underscores are not allowed to prevent confusion with dashes.
151        assert!(PackageName::new("snake_case", "foo").is_err());
152
153        // Slashes are not allowed to avoid ambiguity.
154        assert!(PackageName::new("hello/world", "from/me").is_err());
155
156        // Scopes and names must have one or more characters.
157        assert!(PackageName::new("", "").is_err());
158    }
159
160    #[test]
161    fn parse() {
162        let adopt_me: PackageName = "flub-flab/sisyphus-simulator".parse().unwrap();
163        assert_eq!(adopt_me.scope(), "flub-flab");
164        assert_eq!(adopt_me.name(), "sisyphus-simulator");
165
166        let numbers: PackageName = "123/456".parse().unwrap();
167        assert_eq!(numbers.scope(), "123");
168        assert_eq!(numbers.name(), "456");
169    }
170
171    #[test]
172    fn parse_invalid() {
173        // Extra slashes should result in an error
174        let extra: Result<PackageName, _> = "hello/world/foo".parse();
175        assert!(extra.is_err());
176    }
177
178    #[test]
179    fn display() {
180        let package_name = PackageName::new("evaera", "promise").unwrap();
181        assert_eq!(package_name.to_string(), "evaera/promise");
182    }
183
184    #[test]
185    fn serialization() {
186        let package_name = PackageName::new("lpghatguy", "asink").unwrap();
187        let serialized = serde_json::to_string(&package_name).unwrap();
188        assert_eq!(serialized, "\"lpghatguy/asink\"");
189
190        let deserialized: PackageName = serde_json::from_str(&serialized).unwrap();
191        assert_eq!(deserialized, package_name);
192    }
193}