vise/
validation.rs

1//! Validation logic for label and metric names.
2
3use 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/// Checks that a label name is valid.
72#[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/// Same as [`assert_label_name()`], but for multiple names.
83#[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/// Checks that a metric name is valid.
93#[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/// Checks that a metric prefix is valid.
104#[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}