use std::collections::BTreeMap;
use std::fmt;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct DecisionModel(String);
impl DecisionModel {
pub const LATEST: &'static str = "jev-latest";
pub fn new(route: impl Into<String>) -> Self {
Self(route.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Default for DecisionModel {
fn default() -> Self {
Self(Self::LATEST.to_string())
}
}
impl From<&str> for DecisionModel {
fn from(route: &str) -> Self {
Self(route.to_string())
}
}
impl From<String> for DecisionModel {
fn from(route: String) -> Self {
Self(route)
}
}
impl fmt::Display for DecisionModel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for DecisionModel {
fn as_ref(&self) -> &str {
&self.0
}
}
impl PartialEq<str> for DecisionModel {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for DecisionModel {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct NoulCriteria {
#[serde(rename = "true", default, skip_serializing_if = "Option::is_none")]
pub if_true: Option<String>,
#[serde(rename = "false", default, skip_serializing_if = "Option::is_none")]
pub if_false: Option<String>,
}
impl NoulCriteria {
pub fn is_empty(&self) -> bool {
self.if_true.is_none() && self.if_false.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DecisionQuestion {
Noul {
instructions: Value,
#[serde(default, skip_serializing_if = "NoulCriteria::is_empty")]
criteria: NoulCriteria,
},
Choice {
instructions: Value,
criteria: BTreeMap<String, Option<String>>,
},
Score {
instructions: Value,
criteria: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DecisionRequest {
pub state: Value,
pub model: DecisionModel,
pub questions: BTreeMap<String, DecisionQuestion>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn noul_criteria_empty() {
assert!(NoulCriteria::default().is_empty());
assert!(
!NoulCriteria {
if_true: Some("x".into()),
if_false: None,
}
.is_empty()
);
}
#[test]
fn request_serializes_to_wire_format() {
let mut questions = BTreeMap::new();
questions.insert(
"is_urgent".to_string(),
DecisionQuestion::Noul {
instructions: json!("Does this convey urgency?"),
criteria: NoulCriteria {
if_true: Some("time-sensitive".to_string()),
if_false: None,
},
},
);
let request = DecisionRequest {
state: json!("outage"),
model: "jev-latest".into(),
questions,
};
let wire = serde_json::to_value(&request).unwrap();
assert_eq!(wire["questions"]["is_urgent"]["type"], "noul");
assert_eq!(
wire["questions"]["is_urgent"]["criteria"]["true"],
"time-sensitive"
);
assert!(
wire["questions"]["is_urgent"]["criteria"]
.get("false")
.is_none()
);
}
#[test]
fn choice_and_score_roundtrip() {
let q = DecisionQuestion::Choice {
instructions: json!("team?"),
criteria: BTreeMap::from([("billing".to_string(), Some("pay".to_string()))]),
};
let back: DecisionQuestion =
serde_json::from_value(serde_json::to_value(&q).unwrap()).unwrap();
assert_eq!(q, back);
let q = DecisionQuestion::Score {
instructions: json!("mood?"),
criteria: vec!["Calm".into(), "Angry".into()],
};
let back: DecisionQuestion =
serde_json::from_value(serde_json::to_value(&q).unwrap()).unwrap();
assert_eq!(q, back);
}
}