1use super::Located;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct MemLimit {
8 raw: Located<String>,
9 scalar_kind: MemLimitScalarKind,
10 kind: MemLimitKind,
11}
12
13impl MemLimit {
14 pub(crate) fn parse(raw: Located<String>, scalar_kind: MemLimitScalarKind) -> Self {
15 let value = raw.value();
16 let kind = if matches!(scalar_kind, MemLimitScalarKind::String) && value.contains('$') {
17 MemLimitKind::Expression
18 } else if let Some((amount_raw, unit)) = split_documented_unit(value) {
19 if lexical_zero(amount_raw) {
20 MemLimitKind::Zero {
21 amount_raw: amount_raw.to_owned(),
22 unit: Some(unit),
23 }
24 } else {
25 MemLimitKind::Documented {
26 amount_raw: amount_raw.to_owned(),
27 unit,
28 }
29 }
30 } else if lexical_zero(value) {
31 MemLimitKind::Zero {
32 amount_raw: value.to_owned(),
33 unit: None,
34 }
35 } else {
36 match scalar_kind {
37 MemLimitScalarKind::Number => MemLimitKind::SchemaNumber,
38 MemLimitScalarKind::String => MemLimitKind::ProviderDependentString,
39 }
40 };
41 Self { raw, scalar_kind, kind }
42 }
43
44 #[must_use]
46 pub const fn raw(&self) -> &Located<String> {
47 &self.raw
48 }
49
50 #[must_use]
52 pub const fn scalar_kind(&self) -> MemLimitScalarKind {
53 self.scalar_kind
54 }
55
56 #[must_use]
58 pub const fn kind(&self) -> &MemLimitKind {
59 &self.kind
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65#[non_exhaustive]
66pub enum MemLimitScalarKind {
67 Number,
69 String,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75#[non_exhaustive]
76pub enum MemLimitKind {
77 Documented {
79 amount_raw: String,
81 unit: MemLimitUnit,
83 },
84 Zero {
86 amount_raw: String,
88 unit: Option<MemLimitUnit>,
90 },
91 Expression,
93 SchemaNumber,
95 ProviderDependentString,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
101#[non_exhaustive]
102pub enum MemLimitUnit {
103 B,
105 K,
107 Kb,
109 M,
111 Mb,
113 G,
115 Gb,
117}
118
119impl MemLimitUnit {
120 #[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_mem_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, MemLimitUnit)> {
143 for (suffix, unit) in [
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 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::{MemLimit, MemLimitKind, MemLimitScalarKind, MemLimitUnit, valid_generated_mem_amount};
168 use crate::model::Located;
169 use crate::source::{SourceId, SourceSpan};
170
171 fn classify(value: &str, scalar_kind: MemLimitScalarKind) -> Result<MemLimit, &'static str> {
172 let span = SourceSpan::new(SourceId::new(1), 0, value.len()).ok_or("valid test span expected")?;
173 Ok(MemLimit::parse(Located::new(value.to_owned(), span), scalar_kind))
174 }
175
176 #[test]
177 fn keeps_documented_units_raw_and_schema_forms_distinct() -> Result<(), &'static str> {
178 let bytes = classify("001b", MemLimitScalarKind::String)?;
179 assert!(matches!(
180 bytes.kind(),
181 MemLimitKind::Documented { amount_raw, unit: MemLimitUnit::B } if amount_raw == "001"
182 ));
183 assert_eq!(
184 classify("${LIMIT:-64m}", MemLimitScalarKind::String)?.kind(),
185 &MemLimitKind::Expression
186 );
187 assert_eq!(
188 classify("64", MemLimitScalarKind::Number)?.kind(),
189 &MemLimitKind::SchemaNumber
190 );
191 assert_eq!(
192 classify("64", MemLimitScalarKind::String)?.kind(),
193 &MemLimitKind::ProviderDependentString
194 );
195 assert!(matches!(
196 classify("000mb", MemLimitScalarKind::String)?.kind(),
197 MemLimitKind::Zero { amount_raw, unit: Some(MemLimitUnit::Mb) } if amount_raw == "000"
198 ));
199 Ok(())
200 }
201
202 #[test]
203 fn validates_only_canonical_positive_generated_amounts() {
204 for value in ["1", "64", "18446744073709551616000000000000000000000000000000"] {
205 assert!(valid_generated_mem_amount(value));
206 }
207 for value in ["", "0", "00", "01", "-1", "+1", "1.0", "1e3", " 1", "1 ", "${LIMIT}"] {
208 assert!(!valid_generated_mem_amount(value));
209 }
210 }
211}