torrust_metrics/label/
name.rs1use derive_more::Display;
2use serde::{Deserialize, Serialize};
3
4use crate::prometheus::PrometheusSerializable;
5
6#[derive(Debug, Display, Clone, Eq, PartialEq, Default, Deserialize, Serialize, Hash, Ord, PartialOrd)]
7pub struct LabelName(String);
8
9impl LabelName {
10 #[must_use]
16 pub fn new(name: &str) -> Self {
17 assert!(!name.is_empty(), "Label name cannot be empty.");
18 Self(name.to_owned())
19 }
20}
21
22impl PrometheusSerializable for LabelName {
23 fn to_prometheus(&self) -> String {
40 let processed: String = self
42 .0
43 .chars()
44 .enumerate()
45 .map(|(i, c)| {
46 if i == 0 {
47 if c.is_ascii_alphabetic() || c == '_' { c } else { '_' }
48 } else if c.is_ascii_alphanumeric() || c == '_' {
49 c
50 } else {
51 '_'
52 }
53 })
54 .collect();
55
56 if processed.starts_with("__") && !processed.starts_with("___") {
58 format!("_{processed}")
59 } else {
60 processed
61 }
62 }
63}
64
65#[macro_export]
66macro_rules! label_name {
67 ("") => {
68 compile_error!("Label name cannot be empty");
69 };
70 ($name:literal) => {
71 $crate::label::name::LabelName::new($name)
72 };
73 ($name:ident) => {
74 $crate::label::name::LabelName::new($name)
75 };
76}
77#[cfg(test)]
78mod tests {
79 mod serialization_of_label_name_to_prometheus {
80 use rstest::rstest;
81
82 use crate::label::LabelName;
83 use crate::prometheus::PrometheusSerializable;
84
85 #[rstest]
86 #[case("1 valid name", "valid_name", "valid_name")]
87 #[case("2 leading underscore", "_leading_underscore", "_leading_underscore")]
88 #[case("3 leading lowercase", "v123", "v123")]
89 #[case("4 leading uppercase", "V123", "V123")]
90 fn valid_names_in_prometheus(#[case] case: &str, #[case] input: &str, #[case] output: &str) {
91 assert_eq!(label_name!(input).to_prometheus(), output, "{case} failed: {input:?}");
92 }
93
94 #[rstest]
95 #[case("1 invalid start 1", "9invalid_start", "_invalid_start")]
96 #[case("2 invalid start 2", "@test", "_test")]
97 #[case("3 invalid dash", "invalid-char", "invalid_char")]
98 #[case("4 invalid spaces", "spaces are bad", "spaces_are_bad")]
99 #[case("5 invalid special chars", "a!b@c#d$e%f^g&h*i(j)", "a_b_c_d_e_f_g_h_i_j_")]
100 #[case("6 invalid colon", "my:metric/version", "my_metric_version")]
101 #[case("7 all invalid characters", "!@#$%^&*()", "__________")]
102 #[case("8 non_ascii_characters", "ñaca©", "_aca_")]
103 fn names_that_need_changes_in_prometheus(#[case] case: &str, #[case] input: &str, #[case] output: &str) {
104 assert_eq!(label_name!(input).to_prometheus(), output, "{case} failed: {input:?}");
105 }
106
107 #[rstest]
108 #[case("1 double underscore start", "__private", "___private")]
109 #[case("2 double underscore only", "__", "___")]
110 #[case("3 processed to double underscore", "^^name", "___name")]
111 #[case("4 processed to double underscore after first char", "0__name", "___name")]
112 fn names_starting_with_double_underscore(#[case] case: &str, #[case] input: &str, #[case] output: &str) {
113 assert_eq!(label_name!(input).to_prometheus(), output, "{case} failed: {input:?}");
114 }
115
116 #[test]
117 #[should_panic(expected = "Label name cannot be empty.")]
118 fn empty_name() {
119 let _name = LabelName::new("");
120 }
121 }
122}