1use super::{BooleanValue, FieldReference, Located};
4use crate::source::SourceSpan;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum DependsOn {
9 Short {
11 span: SourceSpan,
13 services: Vec<Located<String>>,
15 },
16 Long {
18 span: SourceSpan,
20 services: Vec<ServiceDependency>,
22 },
23}
24
25impl DependsOn {
26 #[must_use]
28 pub const fn span(&self) -> SourceSpan {
29 match self {
30 Self::Short { span, .. } | Self::Long { span, .. } => *span,
31 }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct ServiceDependency {
38 service: Located<String>,
39 span: SourceSpan,
40 condition: Option<Located<DependencyCondition>>,
41 restart: Option<Located<BooleanValue>>,
42 required: Option<Located<BooleanValue>>,
43 extension_fields: Vec<FieldReference>,
44 unknown_fields: Vec<FieldReference>,
45}
46
47impl ServiceDependency {
48 pub(super) const fn new(service: Located<String>, span: SourceSpan) -> Self {
49 Self {
50 service,
51 span,
52 condition: None,
53 restart: None,
54 required: None,
55 extension_fields: Vec::new(),
56 unknown_fields: Vec::new(),
57 }
58 }
59
60 pub(super) fn set_condition(&mut self, value: Located<DependencyCondition>) {
61 self.condition = Some(value);
62 }
63
64 pub(super) fn set_restart(&mut self, value: Located<BooleanValue>) {
65 self.restart = Some(value);
66 }
67
68 pub(super) fn set_required(&mut self, value: Located<BooleanValue>) {
69 self.required = Some(value);
70 }
71
72 pub(super) fn push_extension(&mut self, field: FieldReference) {
73 self.extension_fields.push(field);
74 }
75
76 pub(super) fn push_unknown(&mut self, field: FieldReference) {
77 self.unknown_fields.push(field);
78 }
79
80 #[must_use]
82 pub const fn service(&self) -> &Located<String> {
83 &self.service
84 }
85
86 #[must_use]
88 pub const fn span(&self) -> SourceSpan {
89 self.span
90 }
91
92 #[must_use]
94 pub const fn condition(&self) -> Option<&Located<DependencyCondition>> {
95 self.condition.as_ref()
96 }
97
98 #[must_use]
100 pub const fn restart(&self) -> Option<&Located<BooleanValue>> {
101 self.restart.as_ref()
102 }
103
104 #[must_use]
106 pub const fn required(&self) -> Option<&Located<BooleanValue>> {
107 self.required.as_ref()
108 }
109
110 #[must_use]
112 pub fn extension_fields(&self) -> &[FieldReference] {
113 &self.extension_fields
114 }
115
116 #[must_use]
118 pub fn unknown_fields(&self) -> &[FieldReference] {
119 &self.unknown_fields
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
125pub enum DependencyCondition {
126 ServiceStarted,
128 ServiceHealthy,
130 ServiceCompletedSuccessfully,
132 Other(String),
134}
135
136impl DependencyCondition {
137 pub(crate) fn parse(value: String) -> Self {
138 match value.as_str() {
139 "service_started" => Self::ServiceStarted,
140 "service_healthy" => Self::ServiceHealthy,
141 "service_completed_successfully" => Self::ServiceCompletedSuccessfully,
142 _ => Self::Other(value),
143 }
144 }
145
146 #[must_use]
148 pub const fn is_known(&self) -> bool {
149 !matches!(self, Self::Other(_))
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
155pub enum HealthcheckDuration {
156 Value(String),
158 Expression(String),
160 Other(String),
162}
163
164impl HealthcheckDuration {
165 pub(crate) fn parse(value: String) -> Self {
166 if value.contains('$') {
167 Self::Expression(value)
168 } else if valid_duration(&value) {
169 Self::Value(value)
170 } else {
171 Self::Other(value)
172 }
173 }
174
175 #[must_use]
177 pub const fn is_valid(&self) -> bool {
178 !matches!(self, Self::Other(_))
179 }
180
181 #[must_use]
183 pub fn raw(&self) -> &str {
184 match self {
185 Self::Value(value) | Self::Expression(value) | Self::Other(value) => value,
186 }
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
192pub enum HealthcheckRetries {
193 Count(String),
195 Expression(String),
197 Other(String),
199}
200
201impl HealthcheckRetries {
202 pub(crate) fn parse(value: String) -> Self {
203 if value.contains('$') {
204 Self::Expression(value)
205 } else if !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()) {
206 Self::Count(value)
207 } else {
208 Self::Other(value)
209 }
210 }
211
212 #[must_use]
214 pub const fn is_valid(&self) -> bool {
215 !matches!(self, Self::Other(_))
216 }
217
218 #[must_use]
220 pub fn raw(&self) -> &str {
221 match self {
222 Self::Count(value) | Self::Expression(value) | Self::Other(value) => value,
223 }
224 }
225}
226
227#[derive(Debug, Clone, PartialEq, Eq)]
229pub struct Healthcheck {
230 span: SourceSpan,
231 test: Option<HealthcheckTest>,
232 interval: Option<Located<HealthcheckDuration>>,
233 timeout: Option<Located<HealthcheckDuration>>,
234 retries: Option<Located<HealthcheckRetries>>,
235 start_period: Option<Located<HealthcheckDuration>>,
236 start_interval: Option<Located<HealthcheckDuration>>,
237 disable: Option<Located<BooleanValue>>,
238 extension_fields: Vec<FieldReference>,
239 unknown_fields: Vec<FieldReference>,
240}
241
242impl Healthcheck {
243 pub(super) const fn new(span: SourceSpan) -> Self {
244 Self {
245 span,
246 test: None,
247 interval: None,
248 timeout: None,
249 retries: None,
250 start_period: None,
251 start_interval: None,
252 disable: None,
253 extension_fields: Vec::new(),
254 unknown_fields: Vec::new(),
255 }
256 }
257
258 pub(super) fn set_test(&mut self, value: HealthcheckTest) {
259 self.test = Some(value);
260 }
261
262 pub(super) fn set_interval(&mut self, value: Located<HealthcheckDuration>) {
263 self.interval = Some(value);
264 }
265
266 pub(super) fn set_timeout(&mut self, value: Located<HealthcheckDuration>) {
267 self.timeout = Some(value);
268 }
269
270 pub(super) fn set_retries(&mut self, value: Located<HealthcheckRetries>) {
271 self.retries = Some(value);
272 }
273
274 pub(super) fn set_start_period(&mut self, value: Located<HealthcheckDuration>) {
275 self.start_period = Some(value);
276 }
277
278 pub(super) fn set_start_interval(&mut self, value: Located<HealthcheckDuration>) {
279 self.start_interval = Some(value);
280 }
281
282 pub(super) fn set_disable(&mut self, value: Located<BooleanValue>) {
283 self.disable = Some(value);
284 }
285
286 pub(super) fn push_extension(&mut self, field: FieldReference) {
287 self.extension_fields.push(field);
288 }
289
290 pub(super) fn push_unknown(&mut self, field: FieldReference) {
291 self.unknown_fields.push(field);
292 }
293
294 #[must_use]
296 pub const fn span(&self) -> SourceSpan {
297 self.span
298 }
299
300 #[must_use]
302 pub const fn test(&self) -> Option<&HealthcheckTest> {
303 self.test.as_ref()
304 }
305
306 #[must_use]
308 pub const fn interval(&self) -> Option<&Located<HealthcheckDuration>> {
309 self.interval.as_ref()
310 }
311
312 #[must_use]
314 pub const fn timeout(&self) -> Option<&Located<HealthcheckDuration>> {
315 self.timeout.as_ref()
316 }
317
318 #[must_use]
320 pub const fn retries(&self) -> Option<&Located<HealthcheckRetries>> {
321 self.retries.as_ref()
322 }
323
324 #[must_use]
326 pub const fn start_period(&self) -> Option<&Located<HealthcheckDuration>> {
327 self.start_period.as_ref()
328 }
329
330 #[must_use]
332 pub const fn start_interval(&self) -> Option<&Located<HealthcheckDuration>> {
333 self.start_interval.as_ref()
334 }
335
336 #[must_use]
338 pub const fn disable(&self) -> Option<&Located<BooleanValue>> {
339 self.disable.as_ref()
340 }
341
342 #[must_use]
344 pub fn is_disabled(&self) -> bool {
345 matches!(
346 self.disable.as_ref().map(Located::value),
347 Some(BooleanValue::Literal(true))
348 ) || matches!(
349 self.test.as_ref().and_then(HealthcheckTest::kind),
350 Some(HealthcheckTestKind::None)
351 )
352 }
353
354 #[must_use]
356 pub fn extension_fields(&self) -> &[FieldReference] {
357 &self.extension_fields
358 }
359
360 #[must_use]
362 pub fn unknown_fields(&self) -> &[FieldReference] {
363 &self.unknown_fields
364 }
365}
366
367#[derive(Debug, Clone, PartialEq, Eq)]
369pub enum HealthcheckTest {
370 String(Located<String>),
372 List {
374 span: SourceSpan,
376 kind: Option<HealthcheckTestKind>,
378 values: Vec<Located<String>>,
380 },
381}
382
383impl HealthcheckTest {
384 #[must_use]
386 pub fn kind(&self) -> Option<HealthcheckTestKind> {
387 match self {
388 Self::String(_) => Some(HealthcheckTestKind::CmdShell),
389 Self::List { kind, .. } => *kind,
390 }
391 }
392
393 #[must_use]
395 pub const fn span(&self) -> SourceSpan {
396 match self {
397 Self::String(value) => value.span(),
398 Self::List { span, .. } => *span,
399 }
400 }
401}
402
403#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
405pub enum HealthcheckTestKind {
406 None,
408 Cmd,
410 CmdShell,
412 Other,
414}
415
416impl HealthcheckTestKind {
417 pub(crate) fn parse(value: &str) -> Self {
418 match value {
419 "NONE" => Self::None,
420 "CMD" => Self::Cmd,
421 "CMD-SHELL" => Self::CmdShell,
422 _ => Self::Other,
423 }
424 }
425}
426
427fn valid_duration(mut value: &str) -> bool {
428 if value == "0" {
429 return true;
430 }
431 let mut found = false;
432 while !value.is_empty() {
433 let number_end = value
434 .char_indices()
435 .take_while(|(_, character)| character.is_ascii_digit() || *character == '.')
436 .map(|(index, character)| index + character.len_utf8())
437 .last()
438 .unwrap_or(0);
439 if number_end == 0 {
440 return false;
441 }
442 let number = &value[..number_end];
443 if number.matches('.').count() > 1 || number == "." {
444 return false;
445 }
446 value = &value[number_end..];
447 let Some(unit) = ["ns", "us", "µs", "μs", "ms", "s", "m", "h"]
448 .into_iter()
449 .find(|unit| value.starts_with(unit))
450 else {
451 return false;
452 };
453 value = &value[unit.len()..];
454 found = true;
455 }
456 found
457}
458
459#[cfg(test)]
460mod tests {
461 use super::valid_duration;
462
463 #[test]
464 fn accepts_compose_duration_segments_without_runtime_parsing() {
465 for value in ["0", "30s", "1m30s", "1.5s", "250ms", "10us"] {
466 assert!(valid_duration(value), "expected valid duration {value}");
467 }
468 for value in ["", "forever", "-1s", "1", "1..5s"] {
469 assert!(!valid_duration(value), "expected invalid duration {value}");
470 }
471 }
472}