Skip to main content

compose_lens/model/
service_runtime.rs

1//! Raw-preserving service resource and namespace values.
2
3use super::{Located, MemLimitUnit};
4use crate::source::SourceSpan;
5
6/// A malformed item retained from a sequence that requires YAML string scalars.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct InvalidServiceStringItem {
9    span: SourceSpan,
10}
11impl InvalidServiceStringItem {
12    pub(crate) const fn new(span: SourceSpan) -> Self {
13        Self { span }
14    }
15    /// Returns the exact malformed item's source span.
16    #[must_use]
17    pub const fn span(self) -> SourceSpan {
18        self.span
19    }
20}
21
22/// The raw spelling of a service integer setting, with a conservative validity classification.
23#[derive(Debug, Clone, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum ServiceInteger {
26    /// An integral YAML scalar in the documented range for its field.
27    Valid(String),
28    /// An integral YAML scalar retained outside the field's documented range.
29    OutOfRange(String),
30    /// A string or scalar expression retained without numeric coercion.
31    Other(String),
32}
33
34impl ServiceInteger {
35    pub(crate) fn parse(value: String, min: i128, max: i128) -> Self {
36        match value.parse::<i128>() {
37            Ok(number) if (min..=max).contains(&number) => Self::Valid(value),
38            Ok(_) => Self::OutOfRange(value),
39            Err(_) => Self::Other(value),
40        }
41    }
42
43    /// Returns whether this is a documented in-range integer spelling.
44    #[must_use]
45    pub const fn is_valid(&self) -> bool {
46        matches!(self, Self::Valid(_))
47    }
48}
49
50/// A service-level Compose `memswap_limit` value with its authored scalar retained.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct MemswapLimit {
53    raw: Located<String>,
54    scalar_kind: MemswapLimitScalarKind,
55    kind: MemswapLimitKind,
56}
57
58impl MemswapLimit {
59    pub(crate) fn parse(raw: Located<String>, scalar_kind: MemswapLimitScalarKind) -> Self {
60        let value = raw.value();
61        let kind = if value == "-1" {
62            MemswapLimitKind::Unlimited
63        } else if matches!(scalar_kind, MemswapLimitScalarKind::String) && value.contains('$') {
64            MemswapLimitKind::Expression
65        } else if let Some((amount_raw, unit)) = quantity_parts(value) {
66            if amount_raw.bytes().all(|byte| byte == b'0') {
67                MemswapLimitKind::Zero {
68                    amount_raw: amount_raw.to_owned(),
69                    unit,
70                }
71            } else {
72                MemswapLimitKind::Positive {
73                    amount_raw: amount_raw.to_owned(),
74                    unit,
75                }
76            }
77        } else {
78            MemswapLimitKind::Other(value.to_owned())
79        };
80        Self { raw, scalar_kind, kind }
81    }
82
83    /// Returns the complete scalar value and its source span without normalization.
84    #[must_use]
85    pub const fn raw(&self) -> &Located<String> {
86        &self.raw
87    }
88
89    /// Returns whether the authored YAML scalar was a number or string.
90    #[must_use]
91    pub const fn scalar_kind(&self) -> MemswapLimitScalarKind {
92        self.scalar_kind
93    }
94
95    /// Returns the raw-preserving memory-plus-swap classification.
96    #[must_use]
97    pub const fn kind(&self) -> &MemswapLimitKind {
98        &self.kind
99    }
100
101    pub(crate) const fn is_positive(&self) -> bool {
102        matches!(self.kind, MemswapLimitKind::Positive { .. })
103    }
104}
105
106/// The YAML scalar category of an authored service memory-plus-swap limit.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
108#[non_exhaustive]
109pub enum MemswapLimitScalarKind {
110    /// A YAML integer or floating-point scalar.
111    Number,
112    /// A YAML string scalar, including quoted numeric spelling.
113    String,
114}
115
116/// The raw-preserving semantic family of a service memory-plus-swap limit.
117#[derive(Debug, Clone, PartialEq, Eq)]
118#[non_exhaustive]
119pub enum MemswapLimitKind {
120    /// Compose's explicit unlimited `-1` spelling.
121    Unlimited,
122    /// An all-zero quantity, kept distinct from omitted and unlimited values.
123    Zero {
124        /// Exact amount spelling before the optional documented unit.
125        amount_raw: String,
126        /// The documented suffix when present.
127        unit: Option<MemLimitUnit>,
128    },
129    /// A positive decimal quantity with an optional documented unit.
130    Positive {
131        /// Exact amount spelling before the optional documented unit.
132        amount_raw: String,
133        /// The documented suffix when present.
134        unit: Option<MemLimitUnit>,
135    },
136    /// A dollar-bearing string deferred to Compose interpolation.
137    Expression,
138    /// A malformed or provider-specific spelling retained for inspection.
139    Other(String),
140}
141
142fn quantity_parts(value: &str) -> Option<(&str, Option<MemLimitUnit>)> {
143    let with_unit = [
144        ("kb", MemLimitUnit::Kb),
145        ("mb", MemLimitUnit::Mb),
146        ("gb", MemLimitUnit::Gb),
147        ("b", MemLimitUnit::B),
148        ("k", MemLimitUnit::K),
149        ("m", MemLimitUnit::M),
150        ("g", MemLimitUnit::G),
151    ]
152    .into_iter()
153    .find_map(|(suffix, unit)| value.strip_suffix(suffix).map(|amount| (amount, Some(unit))));
154    let (amount, unit) = with_unit.unwrap_or((value, None));
155    (!amount.is_empty() && amount.bytes().all(|byte| byte.is_ascii_digit())).then_some((amount, unit))
156}
157
158/// A raw decimal CPU allocation spelling.
159#[derive(Debug, Clone, PartialEq, Eq)]
160#[non_exhaustive]
161pub enum Cpus {
162    /// A decimal spelling, including `0.000`.
163    Decimal(String),
164    /// A deferred interpolation expression.
165    Expression(String),
166    /// A retained non-decimal spelling.
167    Other(String),
168}
169
170impl Cpus {
171    pub(crate) fn parse(value: String) -> Self {
172        if value.contains('$') {
173            return Self::Expression(value);
174        }
175        if !value.is_empty()
176            && value.bytes().all(|byte| byte.is_ascii_digit() || byte == b'.')
177            && value.bytes().filter(|byte| *byte == b'.').count() <= 1
178        {
179            Self::Decimal(value)
180        } else {
181            Self::Other(value)
182        }
183    }
184    /// Returns whether the spelling is decimal or deferred.
185    #[must_use]
186    pub const fn is_valid(&self) -> bool {
187        !matches!(self, Self::Other(_))
188    }
189}
190
191/// A service IPC mode retaining recognized portable service references separately.
192#[derive(Debug, Clone, PartialEq, Eq)]
193#[non_exhaustive]
194pub enum IpcMode {
195    /// A documented shareable IPC mode.
196    Shareable,
197    /// A local service namespace reference.
198    Service(String),
199    /// Any other scalar spelling retained as evidence.
200    Raw(String),
201}
202impl IpcMode {
203    pub(crate) fn parse(value: String) -> Self {
204        if value == "shareable" {
205            Self::Shareable
206        } else if let Some(name) = value.strip_prefix("service:") {
207            Self::Service(name.to_owned())
208        } else {
209            Self::Raw(value)
210        }
211    }
212}
213
214/// A service network mode retaining local namespace reference shapes.
215#[derive(Debug, Clone, PartialEq, Eq)]
216#[non_exhaustive]
217pub enum NetworkMode {
218    /// No network namespace.
219    None,
220    /// The host network namespace.
221    Host,
222    /// A local service namespace reference.
223    Service(String),
224    /// A container namespace reference.
225    Container(String),
226    /// An unclassified raw scalar.
227    Raw(String),
228}
229impl NetworkMode {
230    pub(crate) fn parse(value: String) -> Self {
231        match value.as_str() {
232            "none" => Self::None,
233            "host" => Self::Host,
234            _ if value.starts_with("service:") => Self::Service(value[8..].to_owned()),
235            _ if value.starts_with("container:") => Self::Container(value[10..].to_owned()),
236            _ => Self::Raw(value),
237        }
238    }
239}
240
241/// A PID namespace mode retaining local reference-shaped forms without runtime interpretation.
242#[derive(Debug, Clone, PartialEq, Eq)]
243#[non_exhaustive]
244pub enum PidMode {
245    /// A service namespace reference.
246    Service(String),
247    /// A container namespace reference.
248    Container(String),
249    /// Any other raw scalar.
250    Raw(String),
251}
252impl PidMode {
253    pub(crate) fn parse(value: String) -> Self {
254        if let Some(name) = value.strip_prefix("service:") {
255            Self::Service(name.to_owned())
256        } else if let Some(name) = value.strip_prefix("container:") {
257            Self::Container(name.to_owned())
258        } else {
259            Self::Raw(value)
260        }
261    }
262}
263
264/// A raw `volumes_from` entry with its default access mode made explicit.
265#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct VolumesFrom {
267    raw: Located<String>,
268    source: String,
269    read_only: bool,
270}
271impl VolumesFrom {
272    pub(crate) fn parse(raw: Located<String>) -> Self {
273        let value = raw.value();
274        let (source, read_only) = match value.rsplit_once(':') {
275            Some((source, "ro")) => (source.to_owned(), true),
276            Some((source, "rw")) => (source.to_owned(), false),
277            _ => (value.to_owned(), false),
278        };
279        Self { raw, source, read_only }
280    }
281    /// Returns the original complete entry and span.
282    #[must_use]
283    pub const fn raw(&self) -> &Located<String> {
284        &self.raw
285    }
286    /// Returns the referenced service or container spelling.
287    #[must_use]
288    pub fn source(&self) -> &str {
289        &self.source
290    }
291    /// Returns whether `ro` was requested; omitted access defaults to `rw`.
292    #[must_use]
293    pub const fn read_only(&self) -> bool {
294        self.read_only
295    }
296}