1use crate::error::CoreError as Error;
37use core::{fmt::Display, hash::Hash, str::FromStr};
38use serde::{Deserialize, Serialize};
39
40#[macro_export]
45macro_rules! name_fn {
46 ($vis:vis $fn_name:ident => $name:literal) => {
47 #[inline(always)]
48 $vis fn $fn_name() -> $crate::names::Name {
49 $crate::names::Name::new_unchecked($name)
50 }
51 };
52}
53pub trait StringLike:
58 Clone + Display + PartialEq + Eq + PartialOrd + Ord + Hash + FromStr + AsRef<str> + Into<String>
59{
60 const MAX_LENGTH: usize;
61
62 fn new_unchecked<S: Into<String>>(name: S) -> Self;
63 fn as_str(&self) -> &str;
64 fn is_valid(s: &str) -> bool;
65}
66
67#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
68pub struct Name(String);
69
70#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
71pub struct DisplayName(String);
72
73#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
74pub struct Tag(String);
75
76pub const RFHAM_URN_PREFIX: &str = "urn:rfham:";
77
78name_fn!(pub brand_name_baofeng => "baofeng");
83name_fn!(pub brand_name_chameleon => "chameleon");
84name_fn!(pub brand_name_elecraft => "elecraft");
85name_fn!(pub brand_name_gabil => "gabil");
86name_fn!(pub brand_name_icom => "icom");
87name_fn!(pub brand_name_kenwood => "kenwood");
88name_fn!(pub brand_name_yaesu => "yaesu");
89
90impl Display for Name {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 write!(f, "{}", self.0)
97 }
98}
99
100impl From<Name> for String {
101 fn from(value: Name) -> Self {
102 value.0
103 }
104}
105
106impl AsRef<str> for Name {
107 fn as_ref(&self) -> &str {
108 self.0.as_ref()
109 }
110}
111
112impl FromStr for Name {
113 type Err = Error;
114
115 fn from_str(s: &str) -> Result<Self, Self::Err> {
116 if Self::is_valid(s) {
117 Ok(Self(s.to_ascii_lowercase()))
118 } else {
119 Err(Error::InvalidValueFromStr(s.to_string(), "Name"))
120 }
121 }
122}
123
124impl StringLike for Name {
125 const MAX_LENGTH: usize = 32;
126 fn new_unchecked<S: Into<String>>(name: S) -> Self {
127 Self(name.into())
128 }
129
130 fn as_str(&self) -> &str {
131 self.0.as_str()
132 }
133
134 fn is_valid(s: &str) -> bool {
135 let mut chars = s.chars();
136 !s.is_empty()
137 && s.len() < Self::MAX_LENGTH
138 && chars.next().unwrap().is_ascii_alphabetic()
139 && chars.all(|c| c.is_ascii_alphanumeric() || ['-', '_'].contains(&c))
140 }
141}
142
143impl Display for DisplayName {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 write!(f, "{}", self.0)
150 }
151}
152
153impl From<DisplayName> for String {
154 fn from(value: DisplayName) -> Self {
155 value.0
156 }
157}
158
159impl AsRef<str> for DisplayName {
160 fn as_ref(&self) -> &str {
161 self.0.as_ref()
162 }
163}
164
165impl FromStr for DisplayName {
166 type Err = Error;
167
168 fn from_str(s: &str) -> Result<Self, Self::Err> {
169 if Self::is_valid(s) {
170 Ok(Self(s.to_ascii_lowercase()))
171 } else {
172 Err(Error::InvalidValueFromStr(s.to_string(), "DisplayName"))
173 }
174 }
175}
176
177impl StringLike for DisplayName {
178 const MAX_LENGTH: usize = 48;
179
180 fn new_unchecked<S: Into<String>>(display_name: S) -> Self {
181 Self(display_name.into())
182 }
183
184 fn as_str(&self) -> &str {
185 self.0.as_str()
186 }
187
188 fn is_valid(s: &str) -> bool {
189 s.len() < Self::MAX_LENGTH
190 }
191}
192
193impl Display for Tag {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 write!(f, "{}", self.0)
200 }
201}
202
203impl From<Tag> for String {
204 fn from(value: Tag) -> Self {
205 value.0
206 }
207}
208
209impl AsRef<str> for Tag {
210 fn as_ref(&self) -> &str {
211 self.0.as_ref()
212 }
213}
214
215impl FromStr for Tag {
216 type Err = Error;
217
218 fn from_str(s: &str) -> Result<Self, Self::Err> {
219 if Self::is_valid(s) {
220 Ok(Self(s.to_ascii_lowercase()))
221 } else {
222 Err(Error::InvalidValueFromStr(s.to_string(), "Tag"))
223 }
224 }
225}
226
227impl StringLike for Tag {
228 const MAX_LENGTH: usize = 24;
229
230 fn new_unchecked<S: Into<String>>(tag: S) -> Self {
231 Self(tag.into())
232 }
233
234 fn as_str(&self) -> &str {
235 self.0.as_str()
236 }
237
238 fn is_valid(s: &str) -> bool {
239 !s.is_empty() && s.len() < Self::MAX_LENGTH && s.chars().all(|c| !c.is_whitespace())
240 }
241}
242
243#[cfg(test)]
248mod tests {
249 use super::{DisplayName, Name, Tag};
250 use crate::names::StringLike;
251 use pretty_assertions::assert_eq;
252 use std::str::FromStr;
253
254 #[test]
255 fn name_valid_inputs() {
256 assert!(Name::is_valid("abc"));
257 assert!(Name::is_valid("a-b_c123"));
258 assert!(Name::is_valid("a")); assert!(Name::is_valid("elecraft"));
260 }
261
262 #[test]
263 fn name_invalid_inputs() {
264 assert!(!Name::is_valid("")); assert!(!Name::is_valid("1abc")); assert!(!Name::is_valid("-abc")); assert!(!Name::is_valid(&"a".repeat(32))); }
269
270 #[test]
271 fn name_from_str_lowercases() {
272 let n = Name::from_str("Yaesu").unwrap();
273 assert_eq!(n.as_str(), "yaesu");
274 let n = Name::from_str("ICOM").unwrap();
275 assert_eq!(n.as_str(), "icom");
276 }
277
278 #[test]
279 fn name_from_str_invalid_returns_error() {
280 assert!("9bad".parse::<Name>().is_err());
281 assert!("".parse::<Name>().is_err());
282 }
283
284 #[test]
285 fn display_name_valid() {
286 assert!(DisplayName::is_valid("Yaesu FT-991A"));
287 assert!(DisplayName::is_valid("")); }
289
290 #[test]
291 fn display_name_too_long_is_invalid() {
292 assert!(!DisplayName::is_valid(&"x".repeat(48)));
293 }
294
295 #[test]
296 fn tag_valid_no_whitespace() {
297 assert!(Tag::is_valid("contest"));
298 assert!(Tag::is_valid("dx"));
299 assert!(Tag::is_valid("sota-activation"));
300 }
301
302 #[test]
303 fn tag_invalid_inputs() {
304 assert!(!Tag::is_valid("")); assert!(!Tag::is_valid("has space")); assert!(!Tag::is_valid("tab\there")); assert!(!Tag::is_valid(&"x".repeat(24))); }
309}