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, PartialEq)]
pub enum Rhs {
Value(Value),
Param(String),
Ctx(String),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "RawGuard", into = "RawGuard")]
pub struct Guard {
pub var: String,
pub op: Op,
pub rhs: Rhs,
}
#[derive(Serialize, Deserialize)]
#[serde(
expecting = "a guard mapping with `var`, `op`, and exactly one of `value`, `param`, or `ctx`"
)]
struct RawGuard {
var: String,
op: Op,
#[serde(default, skip_serializing_if = "Option::is_none")]
value: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
param: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
ctx: Option<String>,
}
impl TryFrom<RawGuard> for Guard {
type Error = String;
fn try_from(r: RawGuard) -> Result<Self, Self::Error> {
let rhs = match (r.value, r.param, r.ctx) {
(Some(v), None, None) => Rhs::Value(v),
(None, Some(p), None) => Rhs::Param(p),
(None, None, Some(c)) => Rhs::Ctx(c),
(None, None, None) => {
return Err(format!(
"guard on `{}` needs exactly one of `value`, `param`, or `ctx` (found none)",
r.var
))
}
_ => {
return Err(format!(
"guard on `{}` needs exactly one of `value`, `param`, or `ctx` (found more than one)",
r.var
))
}
};
Ok(Guard {
var: r.var,
op: r.op,
rhs,
})
}
}
impl From<Guard> for RawGuard {
fn from(g: Guard) -> RawGuard {
let (value, param, ctx) = match g.rhs {
Rhs::Value(v) => (Some(v), None, None),
Rhs::Param(p) => (None, Some(p), None),
Rhs::Ctx(c) => (None, None, Some(c)),
};
RawGuard {
var: g.var,
op: g.op,
value,
param,
ctx,
}
}
}
#[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);
}
}