1use indexmap::IndexMap;
7use serde::{Serialize, Serializer};
8use serde_json::Value;
9
10use crate::error::{Error, Result};
11
12#[derive(Debug, Clone, Default, PartialEq, Serialize)]
14#[non_exhaustive]
15#[serde(tag = "type", rename = "noul")]
16pub struct Noul {
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub instructions: Option<Value>,
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub criteria: Option<NoulCriteria>,
23}
24
25#[derive(Debug, Clone, Default, PartialEq, Serialize)]
27#[non_exhaustive]
28pub struct NoulCriteria {
29 #[serde(rename = "true", skip_serializing_if = "Option::is_none")]
31 pub yes: Option<Value>,
32 #[serde(rename = "false", skip_serializing_if = "Option::is_none")]
34 pub no: Option<Value>,
35}
36
37impl Noul {
38 pub fn new(instructions: impl Into<Value>) -> Self {
40 Self {
41 instructions: Some(instructions.into()),
42 criteria: None,
43 }
44 }
45
46 pub fn when_true(mut self, description: impl Into<Value>) -> Self {
48 self.criteria.get_or_insert_with(Default::default).yes = Some(description.into());
49 self
50 }
51
52 pub fn when_false(mut self, description: impl Into<Value>) -> Self {
54 self.criteria.get_or_insert_with(Default::default).no = Some(description.into());
55 self
56 }
57}
58
59#[derive(Debug, Clone, Default, PartialEq, Serialize)]
61#[non_exhaustive]
62#[serde(tag = "type", rename = "choice")]
63pub struct Choice {
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub instructions: Option<Value>,
67 pub criteria: IndexMap<String, Option<Value>>,
69}
70
71impl Choice {
72 pub fn new(instructions: impl Into<Value>) -> Self {
74 Self {
75 instructions: Some(instructions.into()),
76 criteria: IndexMap::new(),
77 }
78 }
79
80 pub fn from_labels<I, S>(instructions: impl Into<Value>, labels: I) -> Self
82 where
83 I: IntoIterator<Item = S>,
84 S: Into<String>,
85 {
86 let mut c = Self::new(instructions);
87 c.criteria
88 .extend(labels.into_iter().map(|l| (l.into(), None)));
89 c
90 }
91
92 pub fn option(mut self, label: impl Into<String>, description: impl Into<Value>) -> Self {
94 self.criteria.insert(label.into(), Some(description.into()));
95 self
96 }
97
98 pub fn label(mut self, label: impl Into<String>) -> Self {
100 self.criteria.insert(label.into(), None);
101 self
102 }
103}
104
105#[derive(Debug, Clone, Default, PartialEq, Serialize)]
107#[non_exhaustive]
108#[serde(tag = "type", rename = "score")]
109pub struct Score {
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub instructions: Option<Value>,
113 pub criteria: Vec<Value>,
115}
116
117impl Score {
118 pub fn new<I, V>(instructions: impl Into<Value>, levels: I) -> Self
120 where
121 I: IntoIterator<Item = V>,
122 V: Into<Value>,
123 {
124 Self {
125 instructions: Some(instructions.into()),
126 criteria: levels.into_iter().map(Into::into).collect(),
127 }
128 }
129
130 pub fn level(mut self, description: impl Into<Value>) -> Self {
132 self.criteria.push(description.into());
133 self
134 }
135}
136
137#[derive(Debug, Clone, PartialEq)]
140#[non_exhaustive]
141pub enum Question {
142 Noul(Noul),
144 Choice(Choice),
146 Score(Score),
148 Raw(Value),
150}
151
152impl Serialize for Question {
153 fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
154 match self {
155 Question::Noul(q) => q.serialize(s),
156 Question::Choice(q) => q.serialize(s),
157 Question::Score(q) => q.serialize(s),
158 Question::Raw(v) => v.serialize(s),
159 }
160 }
161}
162
163impl From<Noul> for Question {
164 fn from(q: Noul) -> Self {
165 Question::Noul(q)
166 }
167}
168impl From<Choice> for Question {
169 fn from(q: Choice) -> Self {
170 Question::Choice(q)
171 }
172}
173impl From<Score> for Question {
174 fn from(q: Score) -> Self {
175 Question::Score(q)
176 }
177}
178impl From<Value> for Question {
179 fn from(v: Value) -> Self {
180 Question::Raw(v)
181 }
182}
183
184#[derive(Debug, Clone, Default, PartialEq, Serialize)]
186#[serde(transparent)]
187pub struct Questions(IndexMap<String, Question>);
188
189impl Questions {
190 pub fn new() -> Self {
192 Self::default()
193 }
194
195 pub fn with(mut self, name: impl Into<String>, question: impl Into<Question>) -> Self {
197 self.insert(name, question);
198 self
199 }
200
201 pub fn insert(&mut self, name: impl Into<String>, question: impl Into<Question>) -> &mut Self {
203 self.0.insert(name.into(), question.into());
204 self
205 }
206
207 pub fn len(&self) -> usize {
209 self.0.len()
210 }
211
212 pub fn is_empty(&self) -> bool {
214 self.0.is_empty()
215 }
216
217 pub fn iter(&self) -> impl Iterator<Item = (&str, &Question)> {
219 self.0.iter().map(|(k, v)| (k.as_str(), v))
220 }
221
222 pub(crate) fn validate(&self) -> Result<()> {
224 if self.0.is_empty() {
225 return Err(Error::InvalidRequest(
226 "At least one question is required.".into(),
227 ));
228 }
229 for (name, q) in &self.0 {
230 match q {
231 Question::Score(s) if s.criteria.is_empty() => return Err(empty_score(name)),
232 Question::Choice(c) if c.criteria.is_empty() => return Err(empty_choice(name)),
233 Question::Raw(v) => validate_raw(name, v)?,
234 _ => {}
235 }
236 }
237 Ok(())
238 }
239}
240
241fn empty_score(name: &str) -> Error {
242 Error::InvalidRequest(format!(
243 "Score question \"{name}\" has no criteria; at least one score is required."
244 ))
245}
246
247fn empty_choice(name: &str) -> Error {
248 Error::InvalidRequest(format!(
249 "Choice question \"{name}\" has no criteria; at least one option is required."
250 ))
251}
252
253fn validate_raw(name: &str, v: &Value) -> Result<()> {
254 let ty = v
255 .as_object()
256 .and_then(|o| o.get("type"))
257 .and_then(Value::as_str)
258 .filter(|t| !t.is_empty())
259 .ok_or_else(|| {
260 Error::InvalidRequest(format!(
261 "Question \"{name}\" must be a question object or a JSON object with a nonempty string \"type\"."
262 ))
263 })?;
264 if matches!(ty, "choice" | "score") {
265 let criteria = v.get("criteria").ok_or_else(|| {
266 Error::InvalidRequest(format!("Question \"{name}\" requires \"criteria\"."))
267 })?;
268 let empty = match criteria {
269 Value::Null => true,
270 Value::Bool(b) => !b,
271 Value::String(s) => s.is_empty(),
272 Value::Array(a) => a.is_empty(),
273 Value::Object(o) => o.is_empty(),
274 Value::Number(n) => n.as_f64() == Some(0.0),
275 };
276 if empty {
277 return Err(if ty == "score" {
278 empty_score(name)
279 } else {
280 empty_choice(name)
281 });
282 }
283 }
284 Ok(())
285}
286
287impl<K: Into<String>, Q: Into<Question>> FromIterator<(K, Q)> for Questions {
288 fn from_iter<T: IntoIterator<Item = (K, Q)>>(iter: T) -> Self {
289 Self(
290 iter.into_iter()
291 .map(|(k, q)| (k.into(), q.into()))
292 .collect(),
293 )
294 }
295}
296
297impl<K: Into<String>, Q: Into<Question>, const N: usize> From<[(K, Q); N]> for Questions {
298 fn from(arr: [(K, Q); N]) -> Self {
299 arr.into_iter().collect()
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306 use serde_json::json;
307
308 #[test]
309 fn serializes_like_the_api_reference() {
310 let q = Questions::new()
311 .with(
312 "department",
313 Choice::new("Which team should handle this")
314 .option("billing", "Payment or subscription issues")
315 .label("other"),
316 )
317 .with(
318 "frustration",
319 Score::new("How frustrated", ["Calm", "Angry"]),
320 )
321 .with(
322 "is_urgent",
323 Noul::new("Urgent?").when_true("Explicitly time-sensitive"),
324 )
325 .with("bare", Noul::default());
326 assert_eq!(
327 serde_json::to_value(&q).unwrap(),
328 json!({
329 "department": {"type": "choice", "instructions": "Which team should handle this",
330 "criteria": {"billing": "Payment or subscription issues", "other": null}},
331 "frustration": {"type": "score", "instructions": "How frustrated", "criteria": ["Calm", "Angry"]},
332 "is_urgent": {"type": "noul", "instructions": "Urgent?", "criteria": {"true": "Explicitly time-sensitive"}},
333 "bare": {"type": "noul"}
334 })
335 );
336 let keys: Vec<_> = q.iter().map(|(k, _)| k).collect();
338 assert_eq!(keys, ["department", "frustration", "is_urgent", "bare"]);
339 }
340
341 #[test]
342 fn structured_instructions() {
343 let q = Score::new(
344 json!({"task": "rate", "focus": ["tone"]}),
345 [json!({"level": "low"}), json!("high")],
346 );
347 assert_eq!(
348 serde_json::to_value(Question::from(q)).unwrap(),
349 json!({"type": "score", "instructions": {"task": "rate", "focus": ["tone"]},
350 "criteria": [{"level": "low"}, "high"]})
351 );
352 }
353
354 #[test]
355 fn validation() {
356 assert!(Questions::new().validate().is_err());
357 assert!(
358 Questions::from([("s", Score::new("x", Vec::<Value>::new()))])
359 .validate()
360 .is_err()
361 );
362 assert!(
363 Questions::from([("r", json!({"instructions": "x"}))])
364 .validate()
365 .is_err()
366 );
367 assert!(
368 Questions::from([("r", json!({"type": ""}))])
369 .validate()
370 .is_err()
371 );
372 assert!(
373 Questions::from([("c", Choice::new("x"))])
374 .validate()
375 .is_err()
376 );
377 assert!(
378 Questions::from([("r", json!({"type": "choice"}))])
379 .validate()
380 .is_err()
381 );
382 assert!(
383 Questions::from([("r", json!({"type": "choice", "criteria": {}}))])
384 .validate()
385 .is_err()
386 );
387 assert!(
388 Questions::from([("r", json!({"type": "score", "criteria": []}))])
389 .validate()
390 .is_err()
391 );
392 assert!(
393 Questions::from([("r", json!({"type": "noul", "future_field": 1}))])
394 .validate()
395 .is_ok()
396 );
397 assert!(
398 Questions::from([("r", json!({"type": "choice", "criteria": {"a": null}}))])
399 .validate()
400 .is_ok()
401 );
402 }
403}