use super::{
ExperimentStatus, RunStatus, EXPERIMENT_STATUS_DRAFT, EXPERIMENT_STATUS_FAILED,
EXPERIMENT_STATUS_FINISHED, EXPERIMENT_STATUS_INTERRUPTED, RUN_STATUS_FAILED,
RUN_STATUS_FINISHED, RUN_STATUS_INTERRUPTED,
};
use serde::de::Error as _;
use serde::ser::SerializeStruct;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ExperimentLifecycle {
Draft,
Finished,
Failed {
reason: Option<String>,
},
Interrupted {
reason: Option<String>,
},
}
impl ExperimentLifecycle {
pub fn status(&self) -> &ExperimentStatus {
match self {
Self::Draft => &ExperimentStatus::Draft,
Self::Finished => &ExperimentStatus::Finished,
Self::Failed { .. } => &ExperimentStatus::Failed,
Self::Interrupted { .. } => &ExperimentStatus::Interrupted,
}
}
pub fn reason(&self) -> Option<&str> {
match self {
Self::Failed { reason } | Self::Interrupted { reason } => reason.as_deref(),
Self::Draft | Self::Finished => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum RunLifecycle {
Finished,
Failed {
reason: Option<String>,
},
Interrupted {
reason: Option<String>,
},
}
impl RunLifecycle {
pub fn status(&self) -> &RunStatus {
match self {
Self::Finished => &RunStatus::Finished,
Self::Failed { .. } => &RunStatus::Failed,
Self::Interrupted { .. } => &RunStatus::Interrupted,
}
}
pub fn reason(&self) -> Option<&str> {
match self {
Self::Failed { reason } | Self::Interrupted { reason } => reason.as_deref(),
Self::Finished => None,
}
}
}
#[derive(Serialize, Deserialize)]
struct LifecycleOutcome {
#[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>,
}
#[derive(Deserialize)]
struct ExperimentLifecycleWire {
status: String,
#[serde(default)]
outcome: Option<LifecycleOutcome>,
}
#[derive(Deserialize)]
struct RunLifecycleWire {
#[serde(default = "default_run_status")]
status: String,
#[serde(default)]
outcome: Option<LifecycleOutcome>,
}
fn serialize_lifecycle<S>(
serializer: S,
status: &'static str,
reason: Option<&str>,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("Lifecycle", usize::from(reason.is_some()) + 1)?;
state.serialize_field("status", status)?;
if let Some(reason) = reason {
state.serialize_field(
"outcome",
&LifecycleOutcome {
reason: Some(reason.to_string()),
},
)?;
}
state.end()
}
impl Serialize for ExperimentLifecycle {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serialize_lifecycle(serializer, self.status().as_str(), self.reason())
}
}
impl<'de> Deserialize<'de> for ExperimentLifecycle {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let wire = ExperimentLifecycleWire::deserialize(deserializer)?;
match (wire.status.as_str(), wire.outcome) {
(EXPERIMENT_STATUS_DRAFT, None) => Ok(Self::Draft),
(EXPERIMENT_STATUS_FINISHED, None) => Ok(Self::Finished),
(EXPERIMENT_STATUS_FAILED, outcome) => Ok(Self::Failed {
reason: outcome.and_then(|outcome| outcome.reason),
}),
(EXPERIMENT_STATUS_INTERRUPTED, outcome) => Ok(Self::Interrupted {
reason: outcome.and_then(|outcome| outcome.reason),
}),
(EXPERIMENT_STATUS_DRAFT | EXPERIMENT_STATUS_FINISHED, Some(_)) => {
Err(D::Error::custom(format_args!(
"Experiment status {} cannot have a lifecycle outcome",
wire.status
)))
}
_ => Err(D::Error::custom(format_args!(
"unknown Experiment status {}",
wire.status
))),
}
}
}
impl Serialize for RunLifecycle {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serialize_lifecycle(serializer, self.status().as_str(), self.reason())
}
}
impl<'de> Deserialize<'de> for RunLifecycle {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let wire = RunLifecycleWire::deserialize(deserializer)?;
match (wire.status.as_str(), wire.outcome) {
(RUN_STATUS_FINISHED, None) => Ok(Self::Finished),
(RUN_STATUS_FAILED, outcome) => Ok(Self::Failed {
reason: outcome.and_then(|outcome| outcome.reason),
}),
(RUN_STATUS_INTERRUPTED, outcome) => Ok(Self::Interrupted {
reason: outcome.and_then(|outcome| outcome.reason),
}),
(RUN_STATUS_FINISHED, Some(_)) => Err(D::Error::custom(
"finished Run cannot have a lifecycle outcome",
)),
_ => Err(D::Error::custom(format_args!(
"unknown Run status {}",
wire.status
))),
}
}
}
fn default_run_status() -> String {
RUN_STATUS_FINISHED.to_string()
}