memfaultd/metrics/
metric_string_key.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
//
// Copyright (c) Memfault, Inc.
// See License.txt for details
use nom::{
    combinator::map_res,
    error::ParseError,
    {AsChar, IResult, InputTakeAtPosition},
};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt::{Debug, Formatter};
use std::str::FromStr;
use std::{borrow::Cow, fmt::Display};

/// Struct containing a valid metric / attribute key.
#[derive(Clone, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct MetricStringKey {
    inner: String,
}

impl MetricStringKey {
    pub fn as_str(&self) -> &str {
        &self.inner
    }

    pub fn with_suffix(&self, suffix: &str) -> Self {
        Self {
            inner: (self.inner.clone() + suffix),
        }
    }

    pub fn metric_string_key_parser<T, E: ParseError<T>>(input: T) -> IResult<T, T, E>
    where
        T: InputTakeAtPosition,
        <T as InputTakeAtPosition>::Item: AsChar,
    {
        input.split_at_position_complete(|item| {
            let c = item.as_char();
            !(c.is_alphanumeric() || c == '_' || c == '/' || c == '.' || c == '-')
        })
    }

    pub fn parse(input: &str) -> IResult<&str, Self> {
        map_res(Self::metric_string_key_parser, Self::from_str)(input)
    }
}

impl Debug for MetricStringKey {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        std::fmt::Debug::fmt(&self.inner, f)
    }
}

impl FromStr for MetricStringKey {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if !(1..=128).contains(&s.len()) {
            return Err("Invalid key: must be between 1 and 128 characters");
        }
        if !s.is_ascii() {
            return Err("Invalid key: must be ASCII");
        }
        Ok(Self {
            inner: s.to_string(),
        })
    }
}

/// Supports creating `MetricStringKey` from a `'static' string. This skips the
/// verification of validity.
/// Ideally we would replace this with a const-compatible verification of
/// validity but it' not available in our currently Rust min version.
impl From<&'static str> for MetricStringKey {
    fn from(value: &'static str) -> Self {
        Self {
            // FIXME: We could really optimize this use-case if inner was a Cow...
            inner: value.to_string(),
        }
    }
}

impl Display for MetricStringKey {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        std::fmt::Display::fmt(&self.inner, f)
    }
}

impl Ord for MetricStringKey {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.inner.cmp(&other.inner)
    }
}
impl PartialOrd for MetricStringKey {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.inner.cmp(&other.inner))
    }
}

impl Serialize for MetricStringKey {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(self.as_str())
    }
}

impl<'de> Deserialize<'de> for MetricStringKey {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<MetricStringKey, D::Error> {
        let s: Cow<str> = Deserialize::deserialize(deserializer)?;
        let key: MetricStringKey = str::parse(&s).map_err(serde::de::Error::custom)?;
        Ok(key)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use rstest::*;

    #[rstest]
    #[case("", "Invalid key: must be between 1 and 128 characters")]
    #[case("\u{1F4A9}", "Invalid key: must be ASCII")]
    fn validation_errors(#[case] input: &str, #[case] expected: &str) {
        let result: Result<MetricStringKey, &str> = str::parse(input);
        assert_eq!(result.err().unwrap(), expected);
    }

    #[rstest]
    #[case("foo")]
    #[case("weird valid.key-123$")]
    fn parsed_ok(#[case] input: &str) {
        let result: Result<MetricStringKey, &str> = str::parse(input);
        assert_eq!(result.ok().unwrap().as_str(), input);
    }
}