1use compile_fmt::{clip, compile_args, compile_panic, fmt, CompileArgs};
4
5const fn is_valid_start_name_char(ch: u8) -> bool {
6 ch == b'_' || ch.is_ascii_lowercase()
7}
8
9const fn is_valid_name_char(ch: u8) -> bool {
10 ch == b'_' || ch.is_ascii_lowercase() || ch.is_ascii_digit()
11}
12
13#[derive(Debug)]
14enum ValidationError {
15 Empty,
16 NonAscii { pos: usize },
17 DisallowedChar { pos: usize, ch: char },
18}
19
20type ErrorArgs = CompileArgs<100>;
21
22impl ValidationError {
23 const fn fmt(self) -> ErrorArgs {
24 match self {
25 Self::Empty => compile_args!(capacity: ErrorArgs::CAPACITY, "name cannot be empty"),
26 Self::NonAscii { pos } => compile_args!(
27 capacity: ErrorArgs::CAPACITY,
28 "name contains non-ASCII chars, first at position ",
29 pos => fmt::<usize>()
30 ),
31 Self::DisallowedChar { pos: 0, ch } => compile_args!(
32 capacity: ErrorArgs::CAPACITY,
33 "name starts with disallowed char '",
34 ch => fmt::<char>(),
35 "'; allowed chars are [_a-z]"
36 ),
37 Self::DisallowedChar { pos, ch } => compile_args!(
38 "name contains a disallowed char '",
39 ch => fmt::<char>(),
40 "' at position ", pos => fmt::<usize>(),
41 "; allowed chars are [_a-z0-9]"
42 ),
43 }
44 }
45}
46
47const fn validate_name(name: &str) -> Result<(), ValidationError> {
48 if name.is_empty() {
49 return Err(ValidationError::Empty);
50 }
51
52 let name_bytes = name.as_bytes();
53 let mut pos = 0;
54 while pos < name.len() {
55 if name_bytes[pos] > 127 {
56 return Err(ValidationError::NonAscii { pos });
57 }
58 let ch = name_bytes[pos];
59 let is_disallowed = (pos == 0 && !is_valid_start_name_char(ch)) || !is_valid_name_char(ch);
60 if is_disallowed {
61 return Err(ValidationError::DisallowedChar {
62 pos,
63 ch: ch as char,
64 });
65 }
66 pos += 1;
67 }
68 Ok(())
69}
70
71#[track_caller]
73pub const fn assert_label_name(name: &str) {
74 if let Err(err) = validate_name(name) {
75 compile_panic!(
76 "Label name `", name => clip(32, "…"), "` is invalid: ",
77 &err.fmt() => fmt::<&ErrorArgs>()
78 );
79 }
80}
81
82#[track_caller]
84pub const fn assert_label_names(names: &[&str]) {
85 let mut idx = 0;
86 while idx < names.len() {
87 assert_label_name(names[idx]);
88 idx += 1;
89 }
90}
91
92#[track_caller]
94pub const fn assert_metric_name(name: &str) {
95 if let Err(err) = validate_name(name) {
96 compile_panic!(
97 "Metric name `", name => clip(32, "…"), "` is invalid: ",
98 &err.fmt() => fmt::<&ErrorArgs>()
99 );
100 }
101}
102
103#[track_caller]
105pub const fn assert_metric_prefix(name: &str) {
106 if let Err(err) = validate_name(name) {
107 compile_panic!(
108 "Metric prefix `", name => clip(32, "…"), "` is invalid: ",
109 &err.fmt() => fmt::<&ErrorArgs>()
110 );
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn validating_names() {
120 let valid_names = ["test", "_private", "snake_case", "l33t_c0d3"];
121 for name in valid_names {
122 validate_name(name).unwrap();
123 }
124
125 validate_name("").unwrap_err();
126 validate_name("нет").unwrap_err();
127 validate_name("t!st").unwrap_err();
128 validate_name("1est").unwrap_err();
129 }
130}