Skip to main content

compose_lens/model/
cpu_count.rs

1//! Raw-preserving service CPU-count values.
2
3/// A service `cpu_count` scalar category with exact authored spelling.
4#[derive(Debug, Clone, PartialEq, Eq)]
5#[non_exhaustive]
6pub enum CpuCount {
7    /// A nonnegative YAML integer retained without fixed-width conversion or normalization.
8    YamlInteger(String),
9    /// A YAML string scalar retained without numeric coercion.
10    String(String),
11    /// A negative YAML integer retained as typed invalid evidence.
12    NegativeYamlInteger(String),
13}
14
15impl CpuCount {
16    pub(crate) fn yaml_integer(raw: String) -> Self {
17        if raw.starts_with('-') && !negative_integer_is_zero(&raw) {
18            Self::NegativeYamlInteger(raw)
19        } else {
20            Self::YamlInteger(raw)
21        }
22    }
23
24    /// Returns whether this scalar satisfies the schema's nonnegative-integer rule.
25    #[must_use]
26    pub const fn is_valid(&self) -> bool {
27        !matches!(self, Self::NegativeYamlInteger(_))
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 negative_integer_is_zero(value: &str) -> bool {
68    let Some(value) = value.strip_prefix('-') else {
69        return false;
70    };
71    let digits = value
72        .strip_prefix("0b")
73        .or_else(|| value.strip_prefix("0o"))
74        .or_else(|| value.strip_prefix("0x"))
75        .unwrap_or(value);
76    digits.bytes().all(|byte| matches!(byte, b'0' | b'_'))
77}
78
79#[cfg(test)]
80mod tests {
81    use super::CpuCount;
82
83    #[test]
84    fn retains_unbounded_integer_spelling_and_negative_zero() {
85        for value in [
86            "0",
87            "-0",
88            "-0x0",
89            "0b1_0",
90            "0o7_7",
91            "0xCA_FE",
92            "999999999999999999999999999999",
93        ] {
94            assert!(CpuCount::yaml_integer(value.to_owned()).is_valid());
95            assert!(CpuCount::yaml_integer_spelling(value));
96        }
97        assert!(matches!(
98            CpuCount::yaml_integer("-1".to_owned()),
99            CpuCount::NegativeYamlInteger(value) if value == "-1"
100        ));
101        for value in ["1.0", "1e3", "0x_1", "1_"] {
102            assert!(!CpuCount::yaml_integer_spelling(value));
103        }
104    }
105}