1use std::collections::{BTreeSet, HashMap};
5use std::hash::Hasher;
6
7use fnv::FnvHasher;
8
9use crate::errors::{Error, Result};
10use crate::metrics::SEPARATOR_BYTE;
11use crate::proto::LabelPair;
12
13fn matches_charset_without_colon(c: char) -> bool {
15 c.is_ascii_alphabetic() || c == '_'
16}
17
18fn matches_charset_with_colon(c: char) -> bool {
20 matches_charset_without_colon(c) || c == ':'
21}
22
23fn is_valid_ident<F: FnMut(char) -> bool>(input: &str, mut charset_validator: F) -> bool {
25 let mut chars = input.chars();
26 let zeroth = chars.next();
27 zeroth
28 .and_then(|zeroth| {
29 if charset_validator(zeroth) {
30 Some(chars.all(|c| charset_validator(c) || c.is_ascii_digit()))
31 } else {
32 None
33 }
34 })
35 .unwrap_or(false)
36}
37
38pub(super) fn is_valid_metric_name(name: &str) -> bool {
41 is_valid_ident(name, matches_charset_with_colon)
42}
43
44pub(super) fn is_valid_label_name(name: &str) -> bool {
45 is_valid_ident(name, matches_charset_without_colon)
46}
47
48#[derive(Clone, Debug)]
61pub struct Desc {
62 pub fq_name: String,
64 pub help: String,
66 pub const_label_pairs: Vec<LabelPair>,
69 pub variable_labels: Vec<String>,
72 pub id: u64,
76 pub dim_hash: u64,
80}
81
82impl Desc {
83 pub fn new(
87 fq_name: String,
88 help: String,
89 variable_labels: Vec<String>,
90 const_labels: HashMap<String, String>,
91 ) -> Result<Desc> {
92 let mut desc = Desc {
93 fq_name: fq_name.clone(),
94 help,
95 const_label_pairs: Vec::with_capacity(const_labels.len()),
96 variable_labels,
97 id: 0,
98 dim_hash: 0,
99 };
100
101 if desc.help.is_empty() {
102 return Err(Error::Msg("empty help string".into()));
103 }
104
105 if !is_valid_metric_name(&desc.fq_name) {
106 return Err(Error::Msg(format!(
107 "'{}' is not a valid metric name",
108 desc.fq_name
109 )));
110 }
111
112 let mut label_values = Vec::with_capacity(const_labels.len() + 1);
113 label_values.push(fq_name);
114
115 let mut label_names = BTreeSet::new();
116
117 for label_name in const_labels.keys() {
118 if !is_valid_label_name(label_name) {
119 return Err(Error::Msg(format!(
120 "'{}' is not a valid label name",
121 &label_name
122 )));
123 }
124
125 if !label_names.insert(label_name.clone()) {
126 return Err(Error::Msg(format!(
127 "duplicate const label name {}",
128 label_name
129 )));
130 }
131 }
132
133 for label_name in &label_names {
135 label_values.push(const_labels.get(label_name).cloned().unwrap());
136 }
137
138 for label_name in &desc.variable_labels {
142 if !is_valid_label_name(label_name) {
143 return Err(Error::Msg(format!(
144 "'{}' is not a valid label name",
145 &label_name
146 )));
147 }
148
149 if !label_names.insert(format!("${}", label_name)) {
150 return Err(Error::Msg(format!(
151 "duplicate variable label name {}",
152 label_name
153 )));
154 }
155 }
156
157 let mut vh = FnvHasher::default();
158 for val in &label_values {
159 vh.write(val.as_bytes());
160 vh.write_u8(SEPARATOR_BYTE);
161 }
162
163 desc.id = vh.finish();
164
165 let mut lh = FnvHasher::default();
168 lh.write(desc.help.as_bytes());
169 lh.write_u8(SEPARATOR_BYTE);
170 for label_name in &label_names {
171 lh.write(label_name.as_bytes());
172 lh.write_u8(SEPARATOR_BYTE);
173 }
174 desc.dim_hash = lh.finish();
175
176 for (key, value) in const_labels {
177 let mut label_pair = LabelPair::default();
178 label_pair.set_name(key);
179 label_pair.set_value(value);
180 desc.const_label_pairs.push(label_pair);
181 }
182
183 desc.const_label_pairs.sort();
184
185 Ok(desc)
186 }
187}
188
189pub trait Describer {
191 fn describe(&self) -> Result<Desc>;
193}
194
195#[cfg(test)]
196mod tests {
197 use std::collections::HashMap;
198
199 use crate::desc::{is_valid_label_name, is_valid_metric_name, Desc};
200 use crate::errors::Error;
201
202 #[test]
203 fn test_is_valid_metric_name() {
204 let tbl = [
205 (":", true),
206 ("_", true),
207 ("a", true),
208 (":9", true),
209 ("_9", true),
210 ("a9", true),
211 ("a_b_9_d:x_", true),
212 ("9", false),
213 ("9:", false),
214 ("9_", false),
215 ("9a", false),
216 ("a-", false),
217 ];
218
219 for &(name, expected) in &tbl {
220 assert_eq!(is_valid_metric_name(name), expected);
221 }
222 }
223
224 #[test]
225 fn test_is_valid_label_name() {
226 let tbl = [
227 ("_", true),
228 ("a", true),
229 ("_9", true),
230 ("a9", true),
231 ("a_b_9_dx_", true),
232 (":", false),
233 (":9", false),
234 ("9", false),
235 ("9:", false),
236 ("9_", false),
237 ("9a", false),
238 ("a-", false),
239 ("a_b_9_d:x_", false),
240 ];
241
242 for &(name, expected) in &tbl {
243 assert_eq!(is_valid_label_name(name), expected);
244 }
245 }
246
247 #[test]
248 fn test_invalid_const_label_name() {
249 for &name in &["-dash", "9gag", ":colon", "colon:", "has space"] {
250 let res = Desc::new(
251 "name".into(),
252 "help".into(),
253 vec![name.into()],
254 HashMap::new(),
255 )
256 .err()
257 .expect(format!("expected error for {}", name).as_ref());
258 match res {
259 Error::Msg(msg) => assert_eq!(msg, format!("'{}' is not a valid label name", name)),
260 other => panic!("{}", other),
261 };
262 }
263 }
264
265 #[test]
266 fn test_invalid_variable_label_name() {
267 for &name in &["-dash", "9gag", ":colon", "colon:", "has space"] {
268 let mut labels = HashMap::new();
269 labels.insert(name.into(), "value".into());
270 let res = Desc::new("name".into(), "help".into(), vec![], labels)
271 .err()
272 .expect(format!("expected error for {}", name).as_ref());
273 match res {
274 Error::Msg(msg) => assert_eq!(msg, format!("'{}' is not a valid label name", name)),
275 other => panic!("{}", other),
276 };
277 }
278 }
279
280 #[test]
281 fn test_invalid_metric_name() {
282 for &name in &["-dash", "9gag", "has space"] {
283 let res = Desc::new(name.into(), "help".into(), vec![], HashMap::new())
284 .err()
285 .expect(format!("expected error for {}", name).as_ref());
286 match res {
287 Error::Msg(msg) => {
288 assert_eq!(msg, format!("'{}' is not a valid metric name", name))
289 }
290 other => panic!("{}", other),
291 };
292 }
293 }
294}