1//! The three question types, with the API's limits enforced when a 2//! question is built, so an over-limit request cannot be constructed. 3 4use std::collections::HashSet; 5 6use serde::Serialize; 7use serde::ser::SerializeMap; 8 9use crate::{Json, ProtocolError}; 10 11/// "You can have a maximum of 255 options per Choice" (api.md). 12pub const MAX_CHOICE_OPTIONS: usize = 255; 13/// "A Score should have at least two levels; the API accepts up to 10." 14pub const SCORE_LEVELS: std::ops::RangeInclusive<usize> = 2..=10; 15 16#[derive(Clone, Debug, PartialEq)] 17pub enum Question { 18 Noul(Noul), 19 Choice(Choice), 20 Score(Score), 21} 22 23/// Yes or no; answered with the probability of yes. 24#[derive(Clone, Debug, PartialEq)] 25pub struct Noul { 26 instructions: Json, 27 yes: Option<Json>, 28 no: Option<Json>, 29} 30 31impl Noul { 32 pub fn new(instructions: Json) -> Self { 33 Noul { instructions, yes: None, no: None } 34 } 35 36 /// What a yes (value near 1) means. 37 pub fn yes_means(mut self, description: Json) -> Self { 38 self.yes = Some(description); 39 self 40 } 41 42 /// What a no (value near 0) means. 43 pub fn no_means(mut self, description: Json) -> Self { 44 self.no = Some(description); 45 self 46 } 47} 48 49/// One label out of 2 to 255. The labels are sent to the model as the 50/// option keys, in the caller's order, which is part of the question 51/// (order bias is measured; contract *SQL surface*). 52#[derive(Clone, Debug, PartialEq)] 53pub struct Choice { 54 instructions: Json, 55 options: Vec<(String, Option<Json>)>, 56} 57 58impl Choice { 59 /// A single option is not a question: answer it locally with 60 /// probability 1 rather than sending it. 61 pub fn new( 62 instructions: Json, 63 options: impl IntoIterator<Item = (String, Option<Json>)>, 64 ) -> Result<Self, ProtocolError> { 65 let options: Vec<_> = options.into_iter().collect(); 66 if options.len() < 2 || options.len() > MAX_CHOICE_OPTIONS { 67 return Err(ProtocolError::Invalid(format!( 68 "a Choice takes 2 to {MAX_CHOICE_OPTIONS} options, not {}", 69 options.len() 70 ))); 71 } 72 let mut seen = HashSet::new(); 73 for (label, _) in &options { 74 if label.is_empty() { 75 return Err(ProtocolError::Invalid("a Choice option label is empty".into())); 76 } 77 if !seen.insert(label.as_str()) { 78 return Err(ProtocolError::Invalid(format!("the Choice option {label:?} appears twice"))); 79 } 80 } 81 Ok(Choice { instructions, options }) 82 } 83 84 pub fn labels(&self) -> impl ExactSizeIterator<Item = &str> { 85 self.options.iter().map(|(l, _)| l.as_str()) 86 } 87} 88 89/// A position on 2 to 10 ordered levels, lowest first. 90#[derive(Clone, Debug, PartialEq)] 91pub struct Score { 92 instructions: Json, 93 levels: Vec<Json>, 94} 95 96impl Score { 97 pub fn new(instructions: Json, levels: impl IntoIterator<Item = Json>) -> Result<Self, ProtocolError> { 98 let levels: Vec<_> = levels.into_iter().collect(); 99 if !SCORE_LEVELS.contains(&levels.len()) { 100 return Err(ProtocolError::Invalid(format!( 101 "a Score takes {} to {} levels, not {}", 102 SCORE_LEVELS.start(), 103 SCORE_LEVELS.end(), 104 levels.len() 105 ))); 106 } 107 Ok(Score { instructions, levels }) 108 } 109 110 pub fn level_count(&self) -> usize { 111 self.levels.len() 112 } 113} 114 115impl Serialize for Question { 116 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> { 117 let mut m = s.serialize_map(None)?; 118 match self { 119 Question::Noul(q) => { 120 m.serialize_entry("type", "noul")?; 121 m.serialize_entry("instructions", &q.instructions)?; 122 if q.yes.is_some() || q.no.is_some() { 123 m.serialize_entry("criteria", &NoulCriteria(q))?; 124 } 125 } 126 Question::Choice(q) => { 127 m.serialize_entry("type", "choice")?; 128 m.serialize_entry("instructions", &q.instructions)?; 129 m.serialize_entry("criteria", &ChoiceCriteria(&q.options))?; 130 } 131 Question::Score(q) => { 132 m.serialize_entry("type", "score")?; 133 m.serialize_entry("instructions", &q.instructions)?; 134 m.serialize_entry("criteria", &q.levels)?; 135 } 136 } 137 m.end() 138 } 139} 140 141struct NoulCriteria<'a>(&'a Noul); 142 143impl Serialize for NoulCriteria<'_> { 144 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> { 145 let mut m = s.serialize_map(None)?; 146 if let Some(yes) = &self.0.yes { 147 m.serialize_entry("true", yes)?; 148 } 149 if let Some(no) = &self.0.no { 150 m.serialize_entry("false", no)?; 151 } 152 m.end() 153 } 154} 155 156/// Written in the caller's order; never through a map type that sorts. 157struct ChoiceCriteria<'a>(&'a [(String, Option<Json>)]); 158 159impl Serialize for ChoiceCriteria<'_> { 160 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> { 161 s.collect_map(self.0.iter().map(|(label, description)| (label, description))) 162 } 163} 164 165#[cfg(test)] 166mod tests { 167 use super::*; 168 169 fn opts(labels: &[&str]) -> Vec<(String, Option<Json>)> { 170 labels.iter().map(|l| (l.to_string(), None)).collect() 171 } 172 173 fn json(q: &Question) -> String { 174 serde_json::to_string(q).unwrap() 175 } 176 177 #[test] 178 fn choice_limits() { 179 let q = Json::text("q"); 180 assert!(Choice::new(q.clone(), opts(&["a"])).is_err()); 181 assert!(Choice::new(q.clone(), opts(&["a", "a"])).is_err()); 182 assert!(Choice::new(q.clone(), opts(&["a", ""])).is_err()); 183 let many: Vec<String> = (0..256).map(|i| i.to_string()).collect(); 184 let many: Vec<&str> = many.iter().map(String::as_str).collect(); 185 assert!(Choice::new(q.clone(), opts(&many[..255])).is_ok()); 186 assert!(Choice::new(q, opts(&many)).is_err()); 187 } 188 189 #[test] 190 fn score_limits() { 191 let level = |n: usize| (0..n).map(|i| Json::text(&i.to_string())).collect::<Vec<_>>(); 192 assert!(Score::new(Json::text("q"), level(1)).is_err()); 193 assert!(Score::new(Json::text("q"), level(2)).is_ok()); 194 assert!(Score::new(Json::text("q"), level(10)).is_ok()); 195 assert!(Score::new(Json::text("q"), level(11)).is_err()); 196 } 197 198 #[test] 199 fn choice_keeps_caller_order() { 200 let q = Choice::new( 201 Json::text("Team?"), 202 [("zeta".to_string(), Some(Json::text("z"))), ("alpha".to_string(), None)], 203 ) 204 .unwrap(); 205 assert_eq!( 206 json(&Question::Choice(q)), 207 r#"{"type":"choice","instructions":"Team?","criteria":{"zeta":"z","alpha":null}}"# 208 ); 209 } 210 211 #[test] 212 fn noul_and_score_shapes() { 213 assert_eq!(json(&Question::Noul(Noul::new(Json::text("U?")))), r#"{"type":"noul","instructions":"U?"}"#); 214 let n = Noul::new(Json::text("U?")).yes_means(Json::text("urgent")); 215 assert_eq!(json(&Question::Noul(n)), r#"{"type":"noul","instructions":"U?","criteria":{"true":"urgent"}}"#); 216 let s = Score::new(Json::text("How?"), [Json::text("calm"), Json::text("angry")]).unwrap(); 217 assert_eq!( 218 json(&Question::Score(s)), 219 r#"{"type":"score","instructions":"How?","criteria":["calm","angry"]}"# 220 ); 221 } 222}