agp_config/component/
id.rs1use regex::Regex;
5use std::fmt;
6use thiserror::Error;
7
8#[derive(Error, Debug)]
9pub enum IdError {
10 #[error("id cannot be emapty")]
11 Empty,
12 #[error("kind contains invalid character(s): {0}")]
13 InvalidCharacter(String),
14 #[error("name part is too long: {0}")]
15 NameTooLong(String),
16 #[error("unknown error")]
17 Unknown,
18}
19
20const KIND_AND_NAME_SEPARATOR: &str = "/";
22
23lazy_static::lazy_static! {
25 static ref KIND_REGEX: Regex = Regex::new(r"^[a-zA-Z][0-9a-zA-Z_]{0,62}$").unwrap();
26 static ref NAME_REGEX: Regex = Regex::new(r"^[^\p{Z}\p{C}\p{S}]+$").unwrap();
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Hash)]
31pub struct Kind {
32 name: String,
33}
34
35impl Kind {
36 pub fn new(ty: &str) -> Result<Self, IdError> {
38 if ty.is_empty() {
39 return Err(IdError::Empty);
40 }
41 if !KIND_REGEX.is_match(ty) {
42 return Err(IdError::InvalidCharacter(ty.to_string()));
43 }
44 Ok(Kind {
45 name: ty.to_string(),
46 })
47 }
48}
49
50impl fmt::Display for Kind {
51 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
52 write!(f, "{}", self.name)
53 }
54}
55
56#[derive(Debug, Hash, PartialEq, Eq, Clone)]
58pub struct ID {
59 kind_val: Kind,
60 name_val: String,
61}
62
63impl ID {
64 pub fn new(kind_val: Kind) -> Self {
66 ID {
67 kind_val,
68 name_val: String::new(),
69 }
70 }
71
72 pub fn new_with_name(kind_val: Kind, name_val: &str) -> Result<Self, IdError> {
74 validate_name(name_val)?;
75 Ok(ID {
76 kind_val,
77 name_val: name_val.to_string(),
78 })
79 }
80
81 pub fn new_with_str(kind_and_name: &str) -> Result<Self, IdError> {
82 let (kind, name) = kind_and_name
83 .split_once(KIND_AND_NAME_SEPARATOR)
84 .unwrap_or(("", ""));
85
86 if kind.is_empty() {
87 return Err(IdError::Empty);
88 }
89
90 let kind_val = Kind::new(kind)?;
91
92 ID::new_with_name(kind_val, name)
93 }
94
95 pub fn kind(&self) -> &Kind {
97 &self.kind_val
98 }
99
100 pub fn name(&self) -> &str {
102 &self.name_val
103 }
104
105 pub fn marshal_text(&self) -> String {
107 self.to_string()
108 }
109}
110
111impl fmt::Display for ID {
113 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
114 if self.name_val.is_empty() {
115 write!(f, "{}", self.kind_val)
116 } else {
117 write!(
118 f,
119 "{}{}{}",
120 self.kind_val, KIND_AND_NAME_SEPARATOR, self.name_val
121 )
122 }
123 }
124}
125
126fn validate_name(name_str: &str) -> Result<(), IdError> {
128 if name_str.is_empty() {
130 return Ok(());
131 }
132
133 if name_str.len() > 1024 {
134 return Err(IdError::NameTooLong(name_str.to_string()));
135 }
136 if !NAME_REGEX.is_match(name_str) {
137 return Err(IdError::InvalidCharacter(name_str.to_string()));
138 }
139 Ok(())
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_kind_creation() {
148 let valid_kind = Kind::new("validKind").unwrap();
149 assert_eq!(valid_kind.to_string(), "validKind");
150
151 assert!(Kind::new("").is_err());
152 assert!(Kind::new("123Invalid").is_err());
153 }
154
155 #[test]
156 fn test_id_creation() {
157 let kind_val = Kind::new("validKind").unwrap();
158
159 let id = ID::new(kind_val.clone());
160 assert_eq!(id.kind().to_string(), "validKind");
161 assert!(id.name().is_empty());
162
163 let id_with_name = ID::new_with_name(kind_val.clone(), "validName").unwrap();
164 assert_eq!(id_with_name.name(), "validName");
165
166 assert!(ID::new_with_name(kind_val.clone(), "").is_ok());
167 assert!(ID::new_with_name(kind_val.clone(), "Invalid Name!").is_err());
168 }
169}