1use alloc::borrow::Cow;
7use core::fmt;
8use core::str::FromStr;
9
10use derive_more::{AsRef, Display, Into};
11use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
12use serde::{Deserialize, Serialize};
13use serde_with::{DeserializeFromStr, SerializeDisplay};
14
15use crate::errors::NameError;
16
17macro_rules! name {
19 ($(#[$doc:meta])* $name:ident, $kind:literal) => {
20 $(#[$doc])*
21 #[derive(
22 Clone,
23 PartialEq,
24 Eq,
25 PartialOrd,
26 Ord,
27 Hash,
28 AsRef,
29 Display,
30 derive_more::Debug,
31 Into,
32 Serialize,
33 Deserialize,
34 JsonSchema,
35 )]
36 #[as_ref(str)]
37 #[debug("{_0:?}")]
38 #[into(String)]
39 #[serde(try_from = "String", into = "String")]
40 #[schemars(inline)]
41 pub struct $name(Box<str>);
42
43 impl $name {
44 #[must_use]
46 pub fn as_str(&self) -> &str {
47 &self.0
48 }
49 }
50
51 impl TryFrom<String> for $name {
52 type Error = NameError;
53
54 fn try_from(name: String) -> Result<Self, NameError> {
55 match check(&name, $kind) {
56 Ok(()) => Ok(Self(name.into_boxed_str())),
57 Err(rule) => Err(NameError { kind: $kind, name, rule }),
58 }
59 }
60 }
61
62 impl FromStr for $name {
63 type Err = NameError;
64
65 fn from_str(name: &str) -> Result<Self, NameError> {
66 Self::try_from(name.to_owned())
67 }
68 }
69
70 impl PartialEq<str> for $name {
71 fn eq(&self, other: &str) -> bool {
72 *self.0 == *other
73 }
74 }
75 };
76}
77
78name! {
79 ProfileName, "profile"
81}
82
83name! {
84 SourceName, "source"
86}
87
88name! {
89 FeatureName, "feature"
91}
92
93name! {
94 ScaffoldName, "scaffold"
96}
97
98fn check(name: &str, kind: &str) -> Result<(), &'static str> {
100 let mut chars = name.chars();
101 if !chars.next().is_some_and(|c| c.is_ascii_alphanumeric()) {
102 return Err("begin with a letter or a digit");
103 }
104 if !chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_')) {
105 return Err("use letters, digits, `-` and `_`");
106 }
107 if kind == "feature" && name == "default" {
108 return Err("`default` names the list of features on by default");
109 }
110 Ok(())
111}
112
113#[derive(
115 Clone,
116 PartialEq,
117 Eq,
118 PartialOrd,
119 Ord,
120 Hash,
121 derive_more::Debug,
122 SerializeDisplay,
123 DeserializeFromStr,
124)]
125#[debug("{self}")]
126pub struct ProfileRef {
127 pub source: SourceName,
129 pub profile: ProfileName,
131}
132
133impl fmt::Display for ProfileRef {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 write!(f, "{}/{}", self.source, self.profile)
136 }
137}
138
139impl FromStr for ProfileRef {
140 type Err = NameError;
141
142 fn from_str(written: &str) -> Result<Self, NameError> {
143 let unqualified = || NameError {
144 kind: "layer",
145 name: written.to_owned(),
146 rule: "write `source/profile`",
147 };
148 let (source, profile) = written.split_once('/').ok_or_else(unqualified)?;
149 Ok(Self { source: source.parse()?, profile: profile.parse()? })
150 }
151}
152
153#[derive(
155 Clone,
156 PartialEq,
157 Eq,
158 PartialOrd,
159 Ord,
160 Hash,
161 derive_more::Debug,
162 SerializeDisplay,
163 DeserializeFromStr,
164)]
165#[debug("{self}")]
166pub struct ScaffoldId {
167 pub profile: ProfileName,
169 pub group: ScaffoldName,
171}
172
173impl fmt::Display for ScaffoldId {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 write!(f, "{}/{}", self.profile, self.group)
176 }
177}
178
179impl FromStr for ScaffoldId {
180 type Err = NameError;
181
182 fn from_str(written: &str) -> Result<Self, NameError> {
183 let unqualified = || NameError {
184 kind: "scaffold",
185 name: written.to_owned(),
186 rule: "write `profile/group`",
187 };
188 let (profile, group) = written.split_once('/').ok_or_else(unqualified)?;
189 Ok(Self { profile: profile.parse()?, group: group.parse()? })
190 }
191}
192
193impl JsonSchema for ProfileRef {
194 fn schema_name() -> Cow<'static, str> {
195 "ProfileRef".into()
196 }
197
198 fn json_schema(_: &mut SchemaGenerator) -> Schema {
199 json_schema!({
200 "description": "A profile in a named source: `source/profile`.",
201 "type": "string",
202 "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]*/[A-Za-z0-9][A-Za-z0-9_-]*$",
203 })
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::{FeatureName, ProfileName, ProfileRef};
210
211 #[test]
212 fn names() {
213 for ok in ["rust", "cargo-deny", "a_b", "x1", "1x"] {
214 assert!(ProfileName::try_from(ok.to_owned()).is_ok(), "{ok} is a name");
215 }
216 for bad in ["", "-x", "_x", "a/b", "a b", "a.b", "é"] {
217 assert!(ProfileName::try_from(bad.to_owned()).is_err(), "{bad:?} is not");
218 }
219 assert!(FeatureName::try_from("default".to_owned()).is_err(), "`default` names the list");
220 assert!(ProfileName::try_from("default".to_owned()).is_ok(), "which a profile may be");
221 }
222
223 #[test]
224 fn profile_refs() {
225 let at: ProfileRef = "atxp/rust".parse().expect("a reference");
226 assert_eq!((at.source.as_str(), at.profile.as_str()), ("atxp", "rust"), "its parts");
227 assert_eq!(at.to_string(), "atxp/rust", "written as read");
228 for bad in ["rust", "atxp/", "/rust", "a/b/c"] {
229 assert!(bad.parse::<ProfileRef>().is_err(), "{bad} is not a reference");
230 }
231 }
232}