use std::collections::BTreeMap;
use std::fmt;
use std::marker::PhantomData;
use ironflow_core::decision::{DecisionModel, DecisionQuestion, DecisionRequest};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::decision::DecisionAnswers;
pub const DEFAULT_DECISION_MODEL: &str = DecisionModel::LATEST;
#[derive(Debug, Clone, Copy)]
pub struct NoAnswers;
#[derive(Serialize, Deserialize)]
#[serde(bound = "")]
pub struct DecisionConfig<T = NoAnswers> {
pub state: Value,
#[serde(default)]
pub model: DecisionModel,
#[serde(default)]
pub questions: BTreeMap<String, DecisionQuestion>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub escalate_below: Option<f64>,
#[serde(skip)]
answers: PhantomData<fn() -> T>,
}
impl<T> fmt::Debug for DecisionConfig<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DecisionConfig")
.field("state", &self.state)
.field("model", &self.model)
.field("questions", &self.questions)
.field("escalate_below", &self.escalate_below)
.finish()
}
}
impl<T> Clone for DecisionConfig<T> {
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
model: self.model.clone(),
questions: self.questions.clone(),
escalate_below: self.escalate_below,
answers: PhantomData,
}
}
}
impl DecisionConfig {
pub fn new(state: impl Serialize) -> Self {
Self {
state: serde_json::to_value(state).unwrap_or(Value::Null),
model: DecisionModel::default(),
questions: BTreeMap::new(),
escalate_below: None,
answers: PhantomData,
}
}
pub fn answers<T: DecisionAnswers>(self) -> DecisionConfig<T> {
DecisionConfig {
state: self.state,
model: self.model,
questions: T::questions(),
escalate_below: self.escalate_below,
answers: PhantomData,
}
}
}
impl<T> DecisionConfig<T> {
pub fn model(mut self, model: impl Into<DecisionModel>) -> Self {
self.model = model.into();
self
}
pub fn escalate_below(mut self, threshold: f64) -> Self {
self.escalate_below = Some(threshold);
self
}
pub fn to_request(&self) -> DecisionRequest {
DecisionRequest {
state: self.state.clone(),
model: self.model.clone(),
questions: self.questions.clone(),
}
}
pub(crate) fn erase(self) -> DecisionConfig {
DecisionConfig {
state: self.state,
model: self.model,
questions: self.questions,
escalate_below: self.escalate_below,
answers: PhantomData,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::decision::{DecisionAnswers, DecisionChoice};
#[derive(DecisionChoice)]
enum Pick {
X,
Y,
}
#[derive(DecisionAnswers)]
#[allow(dead_code)]
struct Probe {
#[noul("yes/no?")]
a: f64,
#[choice("pick")]
b: Pick,
#[score("rate", levels = ["low", "high"])]
c: f64,
}
#[test]
fn answers_assembles_the_questions_of_the_type() {
let config = DecisionConfig::new("state")
.escalate_below(0.6)
.answers::<Probe>();
assert_eq!(config.questions.len(), 3);
assert_eq!(config.escalate_below, Some(0.6));
assert_eq!(config.model, "jev-latest");
}
#[test]
fn builders_keep_working_after_answers() {
let config = DecisionConfig::new("state")
.answers::<Probe>()
.model("jev-2")
.escalate_below(0.5);
assert_eq!(config.model, "jev-2");
assert_eq!(config.escalate_below, Some(0.5));
}
#[test]
fn to_request_carries_state_and_questions() {
let request = DecisionConfig::new("hello").answers::<Probe>().to_request();
assert_eq!(request.state, serde_json::json!("hello"));
assert_eq!(request.questions.len(), 3);
}
#[test]
fn erase_keeps_everything_but_the_type() {
let typed = DecisionConfig::new("s")
.answers::<Probe>()
.escalate_below(0.4);
let questions = typed.questions.clone();
let erased = typed.erase();
assert_eq!(erased.questions, questions);
assert_eq!(erased.escalate_below, Some(0.4));
}
#[test]
fn decision_config_serde_roundtrip() {
let config = DecisionConfig::new("s")
.answers::<Probe>()
.escalate_below(0.5);
let json = serde_json::to_string(&config).unwrap();
assert!(!json.contains("answers"));
let back: DecisionConfig = serde_json::from_str(&json).unwrap();
assert_eq!(back.questions.len(), 3);
assert_eq!(back.escalate_below, Some(0.5));
}
#[test]
fn model_defaults_when_missing_in_json() {
let config: DecisionConfig = serde_json::from_str(r#"{"state":"s"}"#).unwrap();
assert_eq!(config.model, "jev-latest");
assert!(config.questions.is_empty());
}
}