use serde::{Deserialize, Serialize};
#[cfg(feature = "ts")]
use ts_rs::TS;
use crate::compare::{Op, compare};
use crate::executor::template::{TemplateContext, resolve_templates};
#[cfg_attr(feature = "ts", derive(TS))]
#[cfg_attr(
feature = "ts",
ts(
export,
export_to = "../../../../packages/@bnto/nodes/src/generated/definitionTypes/"
)
)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct When {
pub value: String,
pub op: String,
pub expected: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Guard {
Run,
Skip {
reason: String,
},
}
impl When {
pub fn op(&self) -> Result<Op, String> {
Op::parse(&self.op)
.ok_or_else(|| format!("when: 'op' \"{}\" is not one of {}", self.op, Op::NAMES))
}
pub fn describe(&self) -> String {
format!("{} {} {}", self.value, self.op, self.expected)
}
pub fn decide_in(&self, tpl: &TemplateContext) -> Result<Guard, String> {
let mut sides = serde_json::Map::new();
sides.insert("value".into(), self.value.clone().into());
sides.insert("expected".into(), self.expected.clone().into());
let resolved = resolve_templates(&sides, tpl);
self.decide(
&as_text(&resolved["value"]),
&as_text(&resolved["expected"]),
)
}
pub fn decide(&self, value: &str, expected: &str) -> Result<Guard, String> {
Ok(match compare(value, self.op()?, expected) {
Ok(()) => Guard::Run,
Err(why) => Guard::Skip {
reason: format!("when {}: {why}", self.describe()),
},
})
}
}
fn as_text(value: &serde_json::Value) -> String {
match value {
serde_json::Value::String(text) => text.clone(),
other => other.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn when(value: &str, op: &str, expected: &str) -> When {
When {
value: value.into(),
op: op.into(),
expected: expected.into(),
}
}
#[test]
fn a_holding_comparison_runs_the_step() {
let guard = when("{{ctx.platform}}", "equals", "macos").decide("macos", "macos");
assert_eq!(guard, Ok(Guard::Run));
}
#[test]
fn a_failing_comparison_skips_it_and_says_why() {
let guard = when("{{ctx.platform}}", "equals", "macos").decide("linux", "macos");
let Ok(Guard::Skip { reason }) = guard else {
panic!("{guard:?}")
};
assert!(reason.contains("{{ctx.platform}}"), "{reason}");
assert!(reason.contains("linux"), "{reason}");
assert!(reason.contains("macos"), "{reason}");
}
#[test]
fn both_spellings_of_not_equals_work() {
for op in ["not-equals", "not_equals"] {
let guard = when("{{ctx.platform}}", op, "windows").decide("macos", "windows");
assert_eq!(guard, Ok(Guard::Run), "{op}");
}
}
#[test]
fn an_unknown_op_is_an_error() {
let guard = when("a", "is", "a").decide("a", "a");
let Err(message) = guard else {
panic!("{guard:?}")
};
assert!(message.contains("\"is\""), "{message}");
}
#[test]
fn describes_itself_as_written() {
let guard = when("{{ctx.platform}}", "equals", "macos");
assert_eq!(guard.describe(), "{{ctx.platform}} equals macos");
}
#[test]
fn a_guard_without_a_value_is_refused() {
let error = serde_json::from_str::<When>(r#"{"op":"equals","expected":"macos"}"#)
.expect_err("value is required");
assert!(error.to_string().contains("`value`"), "{error}");
}
#[test]
fn reads_and_writes_the_recipe_shape() {
let json = r#"{"value":"{{ctx.platform}}","op":"equals","expected":"macos"}"#;
let parsed: When = serde_json::from_str(json).unwrap();
assert_eq!(parsed, when("{{ctx.platform}}", "equals", "macos"));
assert_eq!(serde_json::to_string(&parsed).unwrap(), json);
}
}