#[allow(clippy::wildcard_imports)]
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LifecycleAction {
Close,
Reopen,
Extend,
Skip,
Pause,
Resume,
AdjustFuture,
Ignore,
}
impl LifecycleAction {
pub const ALL: [Self; 8] = [
Self::Close,
Self::Reopen,
Self::Extend,
Self::Skip,
Self::Pause,
Self::Resume,
Self::AdjustFuture,
Self::Ignore,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Close => "close",
Self::Reopen => "reopen",
Self::Extend => "extend",
Self::Skip => "skip",
Self::Pause => "pause",
Self::Resume => "resume",
Self::AdjustFuture => "adjust-future",
Self::Ignore => "ignore",
}
}
#[must_use]
pub const fn requires_reason(self) -> bool {
matches!(
self,
Self::Close | Self::Reopen | Self::Extend | Self::Skip | Self::AdjustFuture
)
}
#[must_use]
pub const fn writes(self) -> bool {
!matches!(self, Self::Ignore)
}
#[must_use]
pub(super) const fn history_kind(self) -> Option<HistoryEventKind> {
match self {
Self::Close => Some(HistoryEventKind::CycleClosed),
Self::Reopen => Some(HistoryEventKind::CycleReopened),
Self::Extend => Some(HistoryEventKind::CycleExtended),
Self::Skip => Some(HistoryEventKind::CycleSkipped),
Self::Pause => Some(HistoryEventKind::CyclePaused),
Self::Resume => Some(HistoryEventKind::CycleResumed),
Self::AdjustFuture => Some(HistoryEventKind::CycleAdjusted),
Self::Ignore => None,
}
}
pub fn parse(raw: &str) -> Result<Self> {
match raw {
"close" => Ok(Self::Close),
"reopen" => Ok(Self::Reopen),
"extend" => Ok(Self::Extend),
"skip" => Ok(Self::Skip),
"pause" => Ok(Self::Pause),
"resume" => Ok(Self::Resume),
"adjust-future" => Ok(Self::AdjustFuture),
"ignore" => Ok(Self::Ignore),
other => Err(TixError::InvalidInput(format!(
"`{other}` is not a lifecycle action; expected one of `close`, `reopen`, `extend`, \
`skip`, `pause`, `resume`, `adjust-future`, `ignore`"
))),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LifecycleRequest {
pub action: LifecycleAction,
pub schedule_id: CycleScheduleId,
pub cycle_id: Option<GeneratedCycleId>,
pub actor: String,
pub reason: Option<String>,
pub timestamp: Option<String>,
pub confirmed: bool,
pub capacity: Option<CapacityCalculation>,
}
impl LifecycleRequest {
#[must_use]
pub fn new(
action: LifecycleAction,
schedule_id: CycleScheduleId,
actor: impl Into<String>,
) -> Self {
Self {
action,
schedule_id,
cycle_id: None,
actor: actor.into(),
reason: None,
timestamp: None,
confirmed: false,
capacity: None,
}
}
#[must_use]
pub fn with_capacity(mut self, calculation: CapacityCalculation) -> Self {
self.capacity = Some(calculation);
self
}
#[must_use]
pub fn confirm(mut self) -> Self {
self.confirmed = true;
self
}
#[must_use]
pub fn for_cycle(mut self, cycle_id: GeneratedCycleId) -> Self {
self.cycle_id = Some(cycle_id);
self
}
#[must_use]
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
#[must_use]
pub fn with_timestamp(mut self, timestamp: impl Into<String>) -> Self {
self.timestamp = Some(timestamp.into());
self
}
pub(super) fn reason_text(&self) -> &str {
self.reason.as_deref().map(str::trim).unwrap_or_default()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LifecycleDiagnosticKind {
NotConfirmed,
MissingReason,
InvalidReason,
InvalidActor,
UnknownSchedule,
UnknownCycle,
MissingCycle,
CycleNotOwned,
MissingActor,
MissingTimestamp,
InvalidTimestamp,
TerminalSchedule,
TerminalCycle,
MissingCadence,
InvalidCadence,
InvalidDuration,
UnresolvableDate,
MissingSequence,
SnapshotCollision,
ReferenceCollision,
NameCollision,
SchedulingFailed,
ConfigNotPersisted,
}
impl LifecycleDiagnosticKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::NotConfirmed => "not_confirmed",
Self::MissingReason => "missing_reason",
Self::InvalidReason => "invalid_reason",
Self::InvalidActor => "invalid_actor",
Self::UnknownSchedule => "unknown_schedule",
Self::UnknownCycle => "unknown_cycle",
Self::MissingCycle => "missing_cycle",
Self::CycleNotOwned => "cycle_not_owned",
Self::MissingActor => "missing_actor",
Self::MissingTimestamp => "missing_timestamp",
Self::InvalidTimestamp => "invalid_timestamp",
Self::TerminalSchedule => "terminal_schedule",
Self::TerminalCycle => "terminal_cycle",
Self::MissingCadence => "missing_cadence",
Self::InvalidCadence => "invalid_cadence",
Self::InvalidDuration => "invalid_duration",
Self::UnresolvableDate => "unresolvable_date",
Self::MissingSequence => "missing_sequence",
Self::SnapshotCollision => "snapshot_collision",
Self::ReferenceCollision => "reference_collision",
Self::NameCollision => "name_collision",
Self::SchedulingFailed => "scheduling_failure",
Self::ConfigNotPersisted => "config_not_persisted",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LifecycleDiagnostic {
pub kind: LifecycleDiagnosticKind,
pub field: String,
pub message: String,
}
impl LifecycleDiagnostic {
pub(super) fn new(
kind: LifecycleDiagnosticKind,
field: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
kind,
field: field.into(),
message: message.into(),
}
}
#[must_use]
pub fn render(&self) -> String {
render_diagnostics(
"E-tix-cycle-schedule",
std::iter::once((self.kind.as_str(), self.message.as_str())),
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LifecycleOutcome {
Applied {
action: LifecycleAction,
schedule_id: CycleScheduleId,
cycle_id: Option<GeneratedCycleId>,
snapshot_path: Option<String>,
dates: Option<(String, String)>,
message: String,
},
NoOp {
action: LifecycleAction,
schedule_id: CycleScheduleId,
cycle_id: Option<GeneratedCycleId>,
message: String,
},
Rejected {
action: LifecycleAction,
schedule_id: CycleScheduleId,
cycle_id: Option<GeneratedCycleId>,
diagnostics: Vec<LifecycleDiagnostic>,
},
}
impl LifecycleOutcome {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Applied { .. } => "applied",
Self::NoOp { .. } => "no-op",
Self::Rejected { .. } => "rejected",
}
}
#[must_use]
pub fn is_applied(&self) -> bool {
matches!(self, Self::Applied { .. })
}
#[must_use]
pub fn is_noop(&self) -> bool {
matches!(self, Self::NoOp { .. })
}
#[must_use]
pub fn is_rejected(&self) -> bool {
matches!(self, Self::Rejected { .. })
}
#[must_use]
pub fn action(&self) -> LifecycleAction {
match self {
Self::Applied { action, .. }
| Self::NoOp { action, .. }
| Self::Rejected { action, .. } => *action,
}
}
#[must_use]
pub fn schedule_id(&self) -> &CycleScheduleId {
match self {
Self::Applied { schedule_id, .. }
| Self::NoOp { schedule_id, .. }
| Self::Rejected { schedule_id, .. } => schedule_id,
}
}
#[must_use]
pub fn cycle_id(&self) -> Option<&GeneratedCycleId> {
match self {
Self::Applied { cycle_id, .. }
| Self::NoOp { cycle_id, .. }
| Self::Rejected { cycle_id, .. } => cycle_id.as_ref(),
}
}
#[must_use]
pub fn snapshot_path(&self) -> Option<&str> {
match self {
Self::Applied { snapshot_path, .. } => snapshot_path.as_deref(),
Self::NoOp { .. } | Self::Rejected { .. } => None,
}
}
#[must_use]
pub fn dates(&self) -> Option<&(String, String)> {
match self {
Self::Applied { dates, .. } => dates.as_ref(),
Self::NoOp { .. } | Self::Rejected { .. } => None,
}
}
#[must_use]
pub fn diagnostics(&self) -> &[LifecycleDiagnostic] {
match self {
Self::Rejected { diagnostics, .. } => diagnostics,
Self::Applied { .. } | Self::NoOp { .. } => &[],
}
}
#[must_use]
pub fn render(&self) -> String {
match self {
Self::Applied {
action,
cycle_id,
snapshot_path,
dates,
message,
..
} => {
let mut line = format!("{} `{}` applied", action.as_str(), self.schedule_id());
if let Some(cycle_id) = cycle_id {
line.push_str(&format!(" to `{cycle_id}`"));
}
if let Some(path) = snapshot_path {
line.push_str(&format!(" (snapshot `{path}`)"));
}
if let Some((start, due)) = dates {
line.push_str(&format!(" ({start} โ {due})"));
}
format!("{line}: {message}")
}
Self::NoOp {
action, message, ..
} => {
format!(
"{} `{}` no-op: {message}",
action.as_str(),
self.schedule_id()
)
}
Self::Rejected { diagnostics, .. } => {
let first = diagnostics
.first()
.map(LifecycleDiagnostic::render)
.unwrap_or_else(|| "rejected".to_string());
if diagnostics.len() > 1 {
format!("{first} (and {} more)", diagnostics.len() - 1)
} else {
first
}
}
}
}
}