Skip to main content

compose_lens/model/
cpu_percent.rs

1//! Raw-preserving service CPU-percentage values.
2
3/// A service `cpu_percent` scalar category with exact authored spelling.
4#[derive(Debug, Clone, PartialEq, Eq)]
5#[non_exhaustive]
6pub enum CpuPercent {
7    /// A YAML integer in the schema's inclusive `0..=100` range.
8    YamlInteger(String),
9    /// A YAML string scalar retained without numeric coercion.
10    String(String),
11    /// A YAML integer outside the schema's inclusive `0..=100` range, retained as invalid evidence.
12    OutOfRangeYamlInteger(String),
13}
14
15impl CpuPercent {
16    pub(crate) fn yaml_integer(raw: String) -> Self {
17        if yaml_integer_in_percent_range(&raw) {
18            Self::YamlInteger(raw)
19        } else {
20            Self::OutOfRangeYamlInteger(raw)
21        }
22    }
23
24    /// Returns whether this scalar satisfies the schema's inclusive integer range.
25    #[must_use]
26    pub const fn is_valid(&self) -> bool {
27        !matches!(self, Self::OutOfRangeYamlInteger(_))
28    }
29
30    pub(crate) fn yaml_integer_spelling(value: &str) -> bool {
31        let digits = value
32            .strip_prefix('+')
33            .or_else(|| value.strip_prefix('-'))
34            .unwrap_or(value);
35        let (radix, digits) = if let Some(value) = digits.strip_prefix("0b") {
36            (2, value)
37        } else if let Some(value) = digits.strip_prefix("0o") {
38            (8, value)
39        } else if let Some(value) = digits.strip_prefix("0x") {
40            (16, value)
41        } else {
42            (10, digits)
43        };
44        let mut saw_digit = false;
45        let mut previous_separator = false;
46        for byte in digits.bytes() {
47            if byte == b'_' {
48                if !saw_digit || previous_separator {
49                    return false;
50                }
51                previous_separator = true;
52            } else if if radix == 16 {
53                byte.is_ascii_hexdigit()
54            } else {
55                byte.is_ascii_digit() && (byte - b'0') < radix
56            } {
57                saw_digit = true;
58                previous_separator = false;
59            } else {
60                return false;
61            }
62        }
63        saw_digit && !previous_separator
64    }
65}
66
67fn yaml_integer_in_percent_range(value: &str) -> bool {
68    let (negative, value) = match value.strip_prefix('-') {
69        Some(value) => (true, value),
70        None => (false, value.strip_prefix('+').unwrap_or(value)),
71    };
72    let (radix, digits) = if let Some(value) = value.strip_prefix("0b") {
73        (2_u16, value)
74    } else if let Some(value) = value.strip_prefix("0o") {
75        (8_u16, value)
76    } else if let Some(value) = value.strip_prefix("0x") {
77        (16_u16, value)
78    } else {
79        (10_u16, value)
80    };
81
82    let mut result = 0_u16;
83    let mut nonzero = false;
84    for byte in digits.bytes().filter(|byte| *byte != b'_') {
85        let digit = if byte.is_ascii_digit() {
86            u16::from(byte - b'0')
87        } else {
88            u16::from(byte.to_ascii_lowercase() - b'a' + 10)
89        };
90        nonzero |= digit != 0;
91        result = result.saturating_mul(radix).saturating_add(digit);
92        if result > 100 {
93            return false;
94        }
95    }
96    !negative || !nonzero
97}
98
99#[cfg(test)]
100mod tests {
101    use super::CpuPercent;
102
103    #[test]
104    fn classifies_unbounded_yaml_integer_spellings_without_conversion() {
105        for value in ["0", "-0", "-0x0", "+100", "0b110_0100", "0o1_44", "0x64"] {
106            assert!(matches!(
107                CpuPercent::yaml_integer(value.to_owned()),
108                CpuPercent::YamlInteger(actual) if actual == value
109            ));
110            assert!(CpuPercent::yaml_integer_spelling(value));
111        }
112        for value in ["-1", "101", "0x65", "999999999999999999999999999999"] {
113            assert!(matches!(
114                CpuPercent::yaml_integer(value.to_owned()),
115                CpuPercent::OutOfRangeYamlInteger(actual) if actual == value
116            ));
117        }
118        for value in ["1.0", "1e3", "0x_1", "1_"] {
119            assert!(!CpuPercent::yaml_integer_spelling(value));
120        }
121    }
122}