wdl_modules/dependency/
name.rs1use std::str::FromStr;
4
5use serde::Deserialize;
6use serde::Serialize;
7use thiserror::Error;
8
9#[derive(Debug, Error, PartialEq, Eq)]
11#[error("dependency name `{0}` does not match `[A-Za-z][A-Za-z0-9_-]*`")]
12pub struct DependencyNameError(String);
13
14fn is_dependency_name(s: &str) -> bool {
17 let mut chars = s.chars();
18 match chars.next() {
19 Some(c) if c.is_ascii_alphabetic() => {}
20 _ => return false,
21 }
22 chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
23}
24
25#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
38#[serde(into = "String", try_from = "String")]
39pub struct DependencyName {
40 manifest: String,
42 identifier: String,
44}
45
46impl DependencyName {
47 pub fn manifest(&self) -> &str {
49 &self.manifest
50 }
51
52 pub fn identifier(&self) -> &str {
55 &self.identifier
56 }
57
58 pub fn into_manifest(self) -> String {
60 self.manifest
61 }
62
63 pub fn into_identifier(self) -> String {
65 self.identifier
66 }
67}
68
69impl TryFrom<String> for DependencyName {
70 type Error = DependencyNameError;
71
72 fn try_from(s: String) -> Result<Self, Self::Error> {
73 if !is_dependency_name(&s) {
74 return Err(DependencyNameError(s));
75 }
76 let identifier = s.replace('-', "_");
77 if !wdl_grammar::lexer::v1::is_ident(&identifier) {
78 return Err(DependencyNameError(s));
79 }
80 Ok(Self {
81 manifest: s,
82 identifier,
83 })
84 }
85}
86
87impl FromStr for DependencyName {
88 type Err = DependencyNameError;
89
90 fn from_str(s: &str) -> Result<Self, Self::Err> {
91 Self::try_from(s.to_string())
92 }
93}
94
95impl From<DependencyName> for String {
96 fn from(name: DependencyName) -> Self {
97 name.manifest
98 }
99}
100
101impl AsRef<str> for DependencyName {
102 fn as_ref(&self) -> &str {
103 &self.manifest
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn accepts_valid_names() {
113 for name in [
114 "a",
115 "spellbook",
116 "spell_book",
117 "spell-book",
118 "Spell2",
119 "X_1_2_3",
120 "my-crate",
121 ] {
122 assert!(name.parse::<DependencyName>().is_ok(), "rejected `{name}`");
123 }
124 }
125
126 #[test]
127 fn normalizes_hyphens_to_underscores() {
128 let hyphen: DependencyName = "spell-book".parse().unwrap();
129 let underscore: DependencyName = "spell_book".parse().unwrap();
130 assert_eq!(hyphen.identifier(), "spell_book");
131 assert_eq!(hyphen.manifest(), "spell-book");
132 assert_eq!(underscore.manifest(), "spell_book");
133 }
134
135 #[test]
136 fn rejects_invalid_format() {
137 for bad in [
138 "",
139 "1spellbook",
140 "_spellbook",
141 "-spellbook",
142 "spell book",
143 "spell.book",
144 "spell/book",
145 ] {
146 assert!(bad.parse::<DependencyName>().is_err(), "accepted `{bad}`");
147 }
148 }
149
150 #[test]
151 fn rejects_reserved_keywords() {
152 for bad in ["task", "workflow", "import", "if", "as"] {
153 assert!(
154 bad.parse::<DependencyName>().is_err(),
155 "accepted reserved keyword `{bad}` as a dependency name"
156 );
157 }
158 }
159
160 #[test]
161 fn round_trips_via_serde() {
162 let name: DependencyName = "spell-book".parse().unwrap();
163 let json = serde_json::to_string(&name).unwrap();
164 assert_eq!(json, r#""spell-book""#);
165 let parsed: DependencyName = serde_json::from_str(&json).unwrap();
166 assert_eq!(parsed, name);
167 assert_eq!(parsed.manifest(), "spell-book");
168 }
169
170 #[test]
171 fn deserialize_rejects_invalid() {
172 let err = serde_json::from_str::<DependencyName>(r#""1spellbook""#).unwrap_err();
173 assert!(err.to_string().contains("does not match"));
174 }
175}