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#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub struct PackageName {
19 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 assert!(PackageName::new("Upper-Skewer-Case", "Foo").is_err());
149
150 assert!(PackageName::new("snake_case", "foo").is_err());
152
153 assert!(PackageName::new("hello/world", "from/me").is_err());
155
156 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 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}