Skip to main content

compose_lens/model/
shm.rs

1//! Raw-preserving service shared-memory sizes.
2
3use super::Located;
4
5/// A service-level Compose `shm_size` value with its authored scalar retained.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct ShmSize {
8    raw: Located<String>,
9    scalar_kind: ShmSizeScalarKind,
10    kind: ShmSizeKind,
11}
12
13impl ShmSize {
14    pub(crate) fn parse(raw: Located<String>, scalar_kind: ShmSizeScalarKind) -> Self {
15        let value = raw.value();
16        let kind = if matches!(scalar_kind, ShmSizeScalarKind::String) && value.contains('$') {
17            ShmSizeKind::Expression
18        } else if let Some((amount_raw, unit)) = split_documented_unit(value) {
19            if lexical_zero(amount_raw) {
20                ShmSizeKind::Zero {
21                    amount_raw: amount_raw.to_owned(),
22                    unit: Some(unit),
23                }
24            } else {
25                ShmSizeKind::Documented {
26                    amount_raw: amount_raw.to_owned(),
27                    unit,
28                }
29            }
30        } else if lexical_zero(value) {
31            ShmSizeKind::Zero {
32                amount_raw: value.to_owned(),
33                unit: None,
34            }
35        } else {
36            match scalar_kind {
37                ShmSizeScalarKind::Number => ShmSizeKind::ProviderDependentNumber,
38                ShmSizeScalarKind::String => ShmSizeKind::ProviderDependentString,
39            }
40        };
41        Self { raw, scalar_kind, kind }
42    }
43
44    /// Returns the complete scalar value and its source span without normalization.
45    #[must_use]
46    pub const fn raw(&self) -> &Located<String> {
47        &self.raw
48    }
49
50    /// Returns whether the authored YAML scalar was a number or string.
51    #[must_use]
52    pub const fn scalar_kind(&self) -> ShmSizeScalarKind {
53        self.scalar_kind
54    }
55
56    /// Returns the non-destructive shared-memory-size classification.
57    #[must_use]
58    pub const fn kind(&self) -> &ShmSizeKind {
59        &self.kind
60    }
61}
62
63/// The YAML scalar category of an authored service shared-memory size.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65#[non_exhaustive]
66pub enum ShmSizeScalarKind {
67    /// A YAML integer or floating-point scalar.
68    Number,
69    /// A YAML string scalar, including quoted numeric spelling.
70    String,
71}
72
73/// The raw-preserving semantic family of a service shared-memory size.
74#[derive(Debug, Clone, PartialEq, Eq)]
75#[non_exhaustive]
76pub enum ShmSizeKind {
77    /// A string ending in one documented lowercase suffix.
78    Documented {
79        /// Exact text before the suffix; no amount grammar is inferred.
80        amount_raw: String,
81        /// Exact documented suffix family.
82        unit: ShmSizeUnit,
83    },
84    /// An all-zero integral spelling whose Compose semantics are unspecified.
85    Zero {
86        /// Exact all-zero amount spelling.
87        amount_raw: String,
88        /// Documented suffix when one was present.
89        unit: Option<ShmSizeUnit>,
90    },
91    /// A dollar-bearing string deferred to Compose interpolation.
92    Expression,
93    /// A schema-accepted YAML number outside the documented explicit-suffix family.
94    ProviderDependentNumber,
95    /// A schema-accepted YAML string outside the documented lowercase-suffix family.
96    ProviderDependentString,
97}
98
99/// One lowercase byte-unit suffix documented for service `shm_size`.
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
101#[non_exhaustive]
102pub enum ShmSizeUnit {
103    /// Bytes (`b`).
104    B,
105    /// Kilobytes (`k`).
106    K,
107    /// Kilobytes (`kb`).
108    Kb,
109    /// Megabytes (`m`).
110    M,
111    /// Megabytes (`mb`).
112    Mb,
113    /// Gigabytes (`g`).
114    G,
115    /// Gigabytes (`gb`).
116    Gb,
117}
118
119impl ShmSizeUnit {
120    /// Returns the exact lowercase documented suffix.
121    #[must_use]
122    pub const fn as_str(self) -> &'static str {
123        match self {
124            Self::B => "b",
125            Self::K => "k",
126            Self::Kb => "kb",
127            Self::M => "m",
128            Self::Mb => "mb",
129            Self::G => "g",
130            Self::Gb => "gb",
131        }
132    }
133}
134
135pub(crate) fn valid_generated_shm_amount(value: &str) -> bool {
136    value
137        .as_bytes()
138        .split_first()
139        .is_some_and(|(first, rest)| (b'1'..=b'9').contains(first) && rest.iter().all(u8::is_ascii_digit))
140}
141
142fn split_documented_unit(value: &str) -> Option<(&str, ShmSizeUnit)> {
143    for (suffix, unit) in [
144        ("kb", ShmSizeUnit::Kb),
145        ("mb", ShmSizeUnit::Mb),
146        ("gb", ShmSizeUnit::Gb),
147        ("b", ShmSizeUnit::B),
148        ("k", ShmSizeUnit::K),
149        ("m", ShmSizeUnit::M),
150        ("g", ShmSizeUnit::G),
151    ] {
152        if let Some(amount) = value.strip_suffix(suffix) {
153            if !amount.is_empty() {
154                return Some((amount, unit));
155            }
156        }
157    }
158    None
159}
160
161fn lexical_zero(value: &str) -> bool {
162    !value.is_empty() && value.bytes().all(|byte| byte == b'0')
163}
164
165#[cfg(test)]
166mod tests {
167    use super::{ShmSize, ShmSizeKind, ShmSizeScalarKind, ShmSizeUnit, valid_generated_shm_amount};
168    use crate::model::Located;
169    use crate::source::{SourceId, SourceSpan};
170
171    fn classify(value: &str, scalar_kind: ShmSizeScalarKind) -> Result<ShmSize, &'static str> {
172        let span = SourceSpan::new(SourceId::new(1), 0, value.len()).ok_or("valid test span expected")?;
173        Ok(ShmSize::parse(Located::new(value.to_owned(), span), scalar_kind))
174    }
175
176    #[test]
177    fn retains_documented_suffixes_without_inventing_an_amount_grammar() -> Result<(), &'static str> {
178        for (value, amount_raw, unit) in [
179            ("1b", "1", ShmSizeUnit::B),
180            ("01k", "01", ShmSizeUnit::K),
181            ("+1kb", "+1", ShmSizeUnit::Kb),
182            ("1.5m", "1.5", ShmSizeUnit::M),
183            ("1e3mb", "1e3", ShmSizeUnit::Mb),
184            ("-2g", "-2", ShmSizeUnit::G),
185            ("hugegb", "huge", ShmSizeUnit::Gb),
186        ] {
187            let size = classify(value, ShmSizeScalarKind::String)?;
188            assert_eq!(size.raw().value(), value);
189            assert!(matches!(
190                size.kind(),
191                ShmSizeKind::Documented { amount_raw: actual, unit: actual_unit }
192                    if actual == amount_raw && *actual_unit == unit
193            ));
194        }
195        Ok(())
196    }
197
198    #[test]
199    fn keeps_zero_expressions_and_schema_scalar_categories_distinct() -> Result<(), &'static str> {
200        assert!(matches!(
201            classify("000mb", ShmSizeScalarKind::String)?.kind(),
202            ShmSizeKind::Zero { amount_raw, unit: Some(ShmSizeUnit::Mb) } if amount_raw == "000"
203        ));
204        assert!(matches!(
205            classify("0", ShmSizeScalarKind::Number)?.kind(),
206            ShmSizeKind::Zero { amount_raw, unit: None } if amount_raw == "0"
207        ));
208        assert_eq!(
209            classify("${SHM_SIZE:-64m}", ShmSizeScalarKind::String)?.kind(),
210            &ShmSizeKind::Expression
211        );
212        assert_eq!(
213            classify("64", ShmSizeScalarKind::Number)?.kind(),
214            &ShmSizeKind::ProviderDependentNumber
215        );
216        assert_eq!(
217            classify("64", ShmSizeScalarKind::String)?.kind(),
218            &ShmSizeKind::ProviderDependentString
219        );
220        Ok(())
221    }
222
223    #[test]
224    fn validates_only_canonical_positive_generated_amounts() {
225        for value in ["1", "64", "18446744073709551616000000000000000000000000000000"] {
226            assert!(valid_generated_shm_amount(value));
227        }
228        for value in ["", "0", "00", "01", "-1", "+1", "1.0", "1e3", " 1", "1 ", "${SIZE}"] {
229            assert!(!valid_generated_shm_amount(value));
230        }
231    }
232}