use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Value {
Bool(bool),
Int(i64),
Str(String),
}
impl std::fmt::Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Value::Bool(b) => write!(f, "{b}"),
Value::Int(i) => write!(f, "{i}"),
Value::Str(s) => write!(f, "{s}"),
}
}
}
impl Value {
pub fn as_int(&self) -> Option<i64> {
match self {
Value::Int(i) => Some(*i),
Value::Str(s) => s.parse().ok(),
Value::Bool(_) => None,
}
}
pub fn parse_scalar(s: &str) -> Value {
match s {
"true" => Value::Bool(true),
"false" => Value::Bool(false),
_ => match s.parse::<i64>() {
Ok(i) => Value::Int(i),
Err(_) => Value::Str(s.to_string()),
},
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Op {
Eq,
Ne,
Lt,
Lte,
Gt,
Gte,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Guard {
pub var: String,
pub op: Op,
#[serde(default)]
pub value: Option<Value>,
#[serde(default)]
pub param: Option<String>,
#[serde(default)]
pub ctx: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Effect {
Cond {
#[serde(rename = "if")]
cond: Guard,
then: Box<Effect>,
},
Set {
set: String,
to: Value,
},
Incr {
incr: String,
},
Decr {
decr: String,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Transition {
pub to: String,
#[serde(default)]
pub when: Option<String>,
#[serde(default)]
pub blocked_reason: Option<String>,
#[serde(default)]
pub guards: Vec<Guard>,
#[serde(default)]
pub requires: Vec<String>,
#[serde(default)]
pub effects: Vec<Effect>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct State {
#[serde(default)]
pub guidance: String,
#[serde(default)]
pub terminal: bool,
#[serde(default)]
pub transitions: IndexMap<String, Transition>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Definition {
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub params: IndexMap<String, Value>,
#[serde(default)]
pub context: IndexMap<String, Value>,
pub initial: String,
pub states: IndexMap<String, State>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LogEntry {
pub seq: usize,
pub transition: String,
pub from: String,
pub to: String,
#[serde(default)]
pub data: IndexMap<String, Value>,
pub at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Instance {
pub id: String,
pub definition: Definition,
pub params: IndexMap<String, Value>,
pub context: IndexMap<String, Value>,
pub current: String,
pub log: Vec<LogEntry>,
}
#[cfg(test)]
mod tests {
use super::Value;
#[test]
fn parse_scalar_picks_the_most_specific_type() {
assert_eq!(Value::parse_scalar("true"), Value::Bool(true));
assert_eq!(Value::parse_scalar("false"), Value::Bool(false));
assert_eq!(Value::parse_scalar("42"), Value::Int(42));
assert_eq!(Value::parse_scalar("-3"), Value::Int(-3));
assert_eq!(Value::parse_scalar("hello"), Value::Str("hello".into()));
assert_eq!(
Value::parse_scalar("https://x/1"),
Value::Str("https://x/1".into())
);
}
#[test]
fn as_int_coerces_numeric_strings_only() {
assert_eq!(Value::Int(7).as_int(), Some(7));
assert_eq!(Value::Str("7".into()).as_int(), Some(7));
assert_eq!(Value::Str("seven".into()).as_int(), None);
assert_eq!(Value::Bool(true).as_int(), None);
}
}