1use serde::{Deserialize, Deserializer, Serialize, Serializer};
23use serde_json::{Value, json};
24
25use crate::state::State;
26
27pub const DECISION_PREFIX: &str = "decision:";
29pub const DECISION_TURN_KEY: &str = "decision:turn";
31
32pub fn decision_key(question: &str) -> String {
34 format!("{DECISION_PREFIX}{question}")
35}
36
37#[derive(Clone, Debug, PartialEq)]
39pub enum Expect {
40 Yes,
42 No,
44 Is(String),
46 AtLeast(f64),
48 AtMost(f64),
50}
51
52#[derive(Clone, Debug, PartialEq)]
54pub struct Decided {
55 pub question: String,
57 pub expect: Expect,
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "snake_case")]
64pub enum Outcome {
65 Yes,
67 No,
69 Chosen,
71 None,
73 Scored,
75 Unsure,
77}
78
79#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
81pub struct DecisionRecord {
82 pub outcome: Outcome,
84 #[serde(default, skip_serializing_if = "Value::is_null")]
86 pub value: Value,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub confidence: Option<f64>,
90 pub turn: String,
92}
93
94impl DecisionRecord {
95 pub fn current(state: &State, question: &str) -> Option<Self> {
98 let record: Self = state.get(&decision_key(question))?;
99 let turn: String = state.get(DECISION_TURN_KEY)?;
100 (record.turn == turn).then_some(record)
101 }
102}
103
104impl Decided {
105 pub fn holds(&self, state: &State) -> bool {
107 let Some(record) = DecisionRecord::current(state, &self.question) else {
108 return false;
109 };
110 match (&self.expect, record.outcome) {
111 (Expect::Yes, Outcome::Yes | Outcome::Chosen | Outcome::Scored) => true,
112 (Expect::No, Outcome::No | Outcome::None) => true,
113 (Expect::Is(option), Outcome::Chosen) => record.value.as_str() == Some(option),
114 (Expect::AtLeast(x), Outcome::Scored) => record.value.as_f64().is_some_and(|s| s >= *x),
115 (Expect::AtMost(x), Outcome::Scored) => record.value.as_f64().is_some_and(|s| s <= *x),
116 _ => false,
117 }
118 }
119
120 pub fn describe(&self) -> String {
122 let q = &self.question;
123 match &self.expect {
124 Expect::Yes => format!("the caller's latest words must settle '{q}'"),
125 Expect::No => format!("the caller's latest words must rule out '{q}'"),
126 Expect::Is(o) => format!("the caller's latest words must settle '{q}' as '{o}'"),
127 Expect::AtLeast(x) => format!("'{q}' must be at least {x}"),
128 Expect::AtMost(x) => format!("'{q}' must be at most {x}"),
129 }
130 }
131
132 fn to_value(&self) -> Value {
133 let expect = match &self.expect {
134 Expect::Yes => return Value::String(self.question.clone()),
135 Expect::No => json!(false),
136 Expect::Is(o) => json!(o),
137 Expect::AtLeast(x) => json!({ "at_least": x }),
138 Expect::AtMost(x) => json!({ "at_most": x }),
139 };
140 json!({ self.question.clone(): expect })
141 }
142
143 fn from_value(value: Value) -> Result<Self, String> {
144 match value {
145 Value::String(question) => Ok(Self {
146 question,
147 expect: Expect::Yes,
148 }),
149 Value::Object(map) if map.len() == 1 => {
150 let (question, expect) = map.into_iter().next().expect("one entry");
151 let expect = match expect {
152 Value::Bool(true) => Expect::Yes,
153 Value::Bool(false) => Expect::No,
154 Value::String(o) => Expect::Is(o),
155 Value::Object(bound) => match (
156 bound.get("at_least").and_then(Value::as_f64),
157 bound.get("at_most").and_then(Value::as_f64),
158 ) {
159 (Some(x), None) if bound.len() == 1 => Expect::AtLeast(x),
160 (None, Some(x)) if bound.len() == 1 => Expect::AtMost(x),
161 _ => {
162 return Err(format!(
163 "decided '{question}': a score bound is {{\"at_least\": n}} or {{\"at_most\": n}}"
164 ));
165 }
166 },
167 other => {
168 return Err(format!(
169 "decided '{question}': expected true, false, an option or a score bound, not {other}"
170 ));
171 }
172 };
173 Ok(Self { question, expect })
174 }
175 other => Err(format!(
176 "decided takes a question id, or one {{question: expectation}} entry, not {other}"
177 )),
178 }
179 }
180}
181
182impl Serialize for Decided {
183 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
184 self.to_value().serialize(s)
185 }
186}
187
188impl<'de> Deserialize<'de> for Decided {
189 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
190 Self::from_value(Value::deserialize(d)?).map_err(serde::de::Error::custom)
191 }
192}
193
194impl schemars::JsonSchema for Decided {
195 fn schema_name() -> String {
196 "Decided".to_string()
197 }
198 fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
199 #[derive(schemars::JsonSchema)]
200 #[serde(untagged)]
201 #[allow(dead_code)]
202 enum Bound {
203 AtLeast { at_least: f64 },
204 AtMost { at_most: f64 },
205 }
206 #[derive(schemars::JsonSchema)]
207 #[serde(untagged)]
208 #[allow(dead_code)]
209 enum Expectation {
210 Boolean(bool),
212 Option(String),
214 Score(Bound),
216 }
217 #[derive(schemars::JsonSchema)]
220 #[serde(untagged)]
221 #[allow(dead_code)]
222 enum DecidedSchema {
223 Question(String),
224 Expecting(std::collections::BTreeMap<String, Expectation>),
225 }
226 DecidedSchema::json_schema(generator)
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 fn record(state: &State, question: &str, outcome: Outcome, value: Value, turn: &str) {
235 let _ = state.set(
236 decision_key(question),
237 serde_json::to_value(DecisionRecord {
238 outcome,
239 value,
240 confidence: None,
241 turn: turn.into(),
242 })
243 .unwrap(),
244 );
245 }
246
247 #[test]
248 fn json_forms_round_trip() {
249 for (json, expect) in [
250 (json!("confirmed"), Expect::Yes),
251 (json!({ "confirmed": true }), Expect::Yes),
252 (json!({ "confirmed": false }), Expect::No),
253 (json!({ "next": "book" }), Expect::Is("book".into())),
254 (json!({ "anger": { "at_least": 2 } }), Expect::AtLeast(2.0)),
255 (json!({ "anger": { "at_most": 1 } }), Expect::AtMost(1.0)),
256 ] {
257 let d: Decided = serde_json::from_value(json.clone()).unwrap();
258 assert_eq!(d.expect, expect, "{json}");
259 let back: Decided = serde_json::from_value(serde_json::to_value(&d).unwrap()).unwrap();
260 assert_eq!(back, d);
261 }
262 assert!(serde_json::from_value::<Decided>(json!({ "a": 1, "b": 2 })).is_err());
263 assert!(serde_json::from_value::<Decided>(json!({ "a": { "over": 2 } })).is_err());
264 assert!(serde_json::from_value::<Decided>(json!(3)).is_err());
265 }
266
267 #[test]
268 fn an_answer_counts_only_for_its_turn() {
269 let state = State::new();
270 let yes = Decided {
271 question: "confirmed".into(),
272 expect: Expect::Yes,
273 };
274 assert!(!yes.holds(&state), "never asked");
275 let _ = state.set(DECISION_TURN_KEY, "t1");
276 record(&state, "confirmed", Outcome::Yes, Value::Null, "t1");
277 assert!(yes.holds(&state));
278 let _ = state.set(DECISION_TURN_KEY, "t2");
279 assert!(
280 !yes.holds(&state),
281 "a yes from an earlier turn is not a yes now"
282 );
283 }
284
285 #[test]
286 fn expectations_match_outcomes() {
287 let state = State::new();
288 let _ = state.set(DECISION_TURN_KEY, "t");
289 let d = |q: &str, expect| Decided {
290 question: q.into(),
291 expect,
292 };
293 record(&state, "ok", Outcome::No, Value::Null, "t");
294 assert!(d("ok", Expect::No).holds(&state));
295 assert!(!d("ok", Expect::Yes).holds(&state));
296
297 record(&state, "pick", Outcome::Chosen, json!("book"), "t");
298 assert!(d("pick", Expect::Yes).holds(&state));
299 assert!(d("pick", Expect::Is("book".into())).holds(&state));
300 assert!(!d("pick", Expect::Is("stay".into())).holds(&state));
301
302 record(&state, "none", Outcome::None, Value::Null, "t");
303 assert!(d("none", Expect::No).holds(&state));
304 assert!(!d("none", Expect::Yes).holds(&state));
305
306 record(&state, "anger", Outcome::Scored, json!(2.4), "t");
307 assert!(d("anger", Expect::AtLeast(2.0)).holds(&state));
308 assert!(!d("anger", Expect::AtMost(2.0)).holds(&state));
309
310 record(&state, "unsure", Outcome::Unsure, Value::Null, "t");
311 for e in [Expect::Yes, Expect::No] {
312 assert!(!d("unsure", e).holds(&state), "unsure satisfies nothing");
313 }
314 }
315}