use super::{BooleanValue, FieldReference, Located};
use crate::source::SourceSpan;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DependsOn {
Short {
span: SourceSpan,
services: Vec<Located<String>>,
},
Long {
span: SourceSpan,
services: Vec<ServiceDependency>,
},
}
impl DependsOn {
#[must_use]
pub const fn span(&self) -> SourceSpan {
match self {
Self::Short { span, .. } | Self::Long { span, .. } => *span,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServiceDependency {
service: Located<String>,
span: SourceSpan,
condition: Option<Located<DependencyCondition>>,
restart: Option<Located<BooleanValue>>,
required: Option<Located<BooleanValue>>,
extension_fields: Vec<FieldReference>,
unknown_fields: Vec<FieldReference>,
}
impl ServiceDependency {
pub(super) const fn new(service: Located<String>, span: SourceSpan) -> Self {
Self {
service,
span,
condition: None,
restart: None,
required: None,
extension_fields: Vec::new(),
unknown_fields: Vec::new(),
}
}
pub(super) fn set_condition(&mut self, value: Located<DependencyCondition>) {
self.condition = Some(value);
}
pub(super) fn set_restart(&mut self, value: Located<BooleanValue>) {
self.restart = Some(value);
}
pub(super) fn set_required(&mut self, value: Located<BooleanValue>) {
self.required = Some(value);
}
pub(super) fn push_extension(&mut self, field: FieldReference) {
self.extension_fields.push(field);
}
pub(super) fn push_unknown(&mut self, field: FieldReference) {
self.unknown_fields.push(field);
}
#[must_use]
pub const fn service(&self) -> &Located<String> {
&self.service
}
#[must_use]
pub const fn span(&self) -> SourceSpan {
self.span
}
#[must_use]
pub const fn condition(&self) -> Option<&Located<DependencyCondition>> {
self.condition.as_ref()
}
#[must_use]
pub const fn restart(&self) -> Option<&Located<BooleanValue>> {
self.restart.as_ref()
}
#[must_use]
pub const fn required(&self) -> Option<&Located<BooleanValue>> {
self.required.as_ref()
}
#[must_use]
pub fn extension_fields(&self) -> &[FieldReference] {
&self.extension_fields
}
#[must_use]
pub fn unknown_fields(&self) -> &[FieldReference] {
&self.unknown_fields
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DependencyCondition {
ServiceStarted,
ServiceHealthy,
ServiceCompletedSuccessfully,
Other(String),
}
impl DependencyCondition {
pub(super) fn parse(value: String) -> Self {
match value.as_str() {
"service_started" => Self::ServiceStarted,
"service_healthy" => Self::ServiceHealthy,
"service_completed_successfully" => Self::ServiceCompletedSuccessfully,
_ => Self::Other(value),
}
}
#[must_use]
pub const fn is_known(&self) -> bool {
!matches!(self, Self::Other(_))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HealthcheckDuration {
Value(String),
Expression(String),
Other(String),
}
impl HealthcheckDuration {
pub(super) fn parse(value: String) -> Self {
if value.contains('$') {
Self::Expression(value)
} else if valid_duration(&value) {
Self::Value(value)
} else {
Self::Other(value)
}
}
#[must_use]
pub const fn is_valid(&self) -> bool {
!matches!(self, Self::Other(_))
}
#[must_use]
pub fn raw(&self) -> &str {
match self {
Self::Value(value) | Self::Expression(value) | Self::Other(value) => value,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HealthcheckRetries {
Count(String),
Expression(String),
Other(String),
}
impl HealthcheckRetries {
pub(super) fn parse(value: String) -> Self {
if value.contains('$') {
Self::Expression(value)
} else if !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()) {
Self::Count(value)
} else {
Self::Other(value)
}
}
#[must_use]
pub const fn is_valid(&self) -> bool {
!matches!(self, Self::Other(_))
}
#[must_use]
pub fn raw(&self) -> &str {
match self {
Self::Count(value) | Self::Expression(value) | Self::Other(value) => value,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Healthcheck {
span: SourceSpan,
test: Option<HealthcheckTest>,
interval: Option<Located<HealthcheckDuration>>,
timeout: Option<Located<HealthcheckDuration>>,
retries: Option<Located<HealthcheckRetries>>,
start_period: Option<Located<HealthcheckDuration>>,
start_interval: Option<Located<HealthcheckDuration>>,
disable: Option<Located<BooleanValue>>,
extension_fields: Vec<FieldReference>,
unknown_fields: Vec<FieldReference>,
}
impl Healthcheck {
pub(super) const fn new(span: SourceSpan) -> Self {
Self {
span,
test: None,
interval: None,
timeout: None,
retries: None,
start_period: None,
start_interval: None,
disable: None,
extension_fields: Vec::new(),
unknown_fields: Vec::new(),
}
}
pub(super) fn set_test(&mut self, value: HealthcheckTest) {
self.test = Some(value);
}
pub(super) fn set_interval(&mut self, value: Located<HealthcheckDuration>) {
self.interval = Some(value);
}
pub(super) fn set_timeout(&mut self, value: Located<HealthcheckDuration>) {
self.timeout = Some(value);
}
pub(super) fn set_retries(&mut self, value: Located<HealthcheckRetries>) {
self.retries = Some(value);
}
pub(super) fn set_start_period(&mut self, value: Located<HealthcheckDuration>) {
self.start_period = Some(value);
}
pub(super) fn set_start_interval(&mut self, value: Located<HealthcheckDuration>) {
self.start_interval = Some(value);
}
pub(super) fn set_disable(&mut self, value: Located<BooleanValue>) {
self.disable = Some(value);
}
pub(super) fn push_extension(&mut self, field: FieldReference) {
self.extension_fields.push(field);
}
pub(super) fn push_unknown(&mut self, field: FieldReference) {
self.unknown_fields.push(field);
}
#[must_use]
pub const fn span(&self) -> SourceSpan {
self.span
}
#[must_use]
pub const fn test(&self) -> Option<&HealthcheckTest> {
self.test.as_ref()
}
#[must_use]
pub const fn interval(&self) -> Option<&Located<HealthcheckDuration>> {
self.interval.as_ref()
}
#[must_use]
pub const fn timeout(&self) -> Option<&Located<HealthcheckDuration>> {
self.timeout.as_ref()
}
#[must_use]
pub const fn retries(&self) -> Option<&Located<HealthcheckRetries>> {
self.retries.as_ref()
}
#[must_use]
pub const fn start_period(&self) -> Option<&Located<HealthcheckDuration>> {
self.start_period.as_ref()
}
#[must_use]
pub const fn start_interval(&self) -> Option<&Located<HealthcheckDuration>> {
self.start_interval.as_ref()
}
#[must_use]
pub const fn disable(&self) -> Option<&Located<BooleanValue>> {
self.disable.as_ref()
}
#[must_use]
pub fn is_disabled(&self) -> bool {
matches!(
self.disable.as_ref().map(Located::value),
Some(BooleanValue::Literal(true))
) || matches!(
self.test.as_ref().and_then(HealthcheckTest::kind),
Some(HealthcheckTestKind::None)
)
}
#[must_use]
pub fn extension_fields(&self) -> &[FieldReference] {
&self.extension_fields
}
#[must_use]
pub fn unknown_fields(&self) -> &[FieldReference] {
&self.unknown_fields
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HealthcheckTest {
String(Located<String>),
List {
span: SourceSpan,
kind: Option<HealthcheckTestKind>,
values: Vec<Located<String>>,
},
}
impl HealthcheckTest {
#[must_use]
pub fn kind(&self) -> Option<HealthcheckTestKind> {
match self {
Self::String(_) => Some(HealthcheckTestKind::CmdShell),
Self::List { kind, .. } => *kind,
}
}
#[must_use]
pub const fn span(&self) -> SourceSpan {
match self {
Self::String(value) => value.span(),
Self::List { span, .. } => *span,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HealthcheckTestKind {
None,
Cmd,
CmdShell,
Other,
}
impl HealthcheckTestKind {
pub(super) fn parse(value: &str) -> Self {
match value {
"NONE" => Self::None,
"CMD" => Self::Cmd,
"CMD-SHELL" => Self::CmdShell,
_ => Self::Other,
}
}
}
fn valid_duration(mut value: &str) -> bool {
if value == "0" {
return true;
}
let mut found = false;
while !value.is_empty() {
let number_end = value
.char_indices()
.take_while(|(_, character)| character.is_ascii_digit() || *character == '.')
.map(|(index, character)| index + character.len_utf8())
.last()
.unwrap_or(0);
if number_end == 0 {
return false;
}
let number = &value[..number_end];
if number.matches('.').count() > 1 || number == "." {
return false;
}
value = &value[number_end..];
let Some(unit) = ["ns", "us", "µs", "μs", "ms", "s", "m", "h"]
.into_iter()
.find(|unit| value.starts_with(unit))
else {
return false;
};
value = &value[unit.len()..];
found = true;
}
found
}
#[cfg(test)]
mod tests {
use super::valid_duration;
#[test]
fn accepts_compose_duration_segments_without_runtime_parsing() {
for value in ["0", "30s", "1m30s", "1.5s", "250ms", "10us"] {
assert!(valid_duration(value), "expected valid duration {value}");
}
for value in ["", "forever", "-1s", "1", "1..5s"] {
assert!(!valid_duration(value), "expected invalid duration {value}");
}
}
}