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}