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 #[must_use]
48 pub fn when_true(mut self, description: impl Into<Value>) -> Self {
49 self.criteria.get_or_insert_with(Default::default).yes = Some(description.into());
50 self
51 }
52
53 #[must_use]
55 pub fn when_false(mut self, description: impl Into<Value>) -> Self {
56 self.criteria.get_or_insert_with(Default::default).no = Some(description.into());
57 self
58 }
59}
60
61#[derive(Debug, Clone, Default, PartialEq, Serialize)]
63#[non_exhaustive]
64#[serde(tag = "type", rename = "choice")]
65pub struct Choice {
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub instructions: Option<Value>,
69 pub criteria: IndexMap<String, Option<Value>>,
71}
72
73impl Choice {
74 pub fn new(instructions: impl Into<Value>) -> Self {
76 Self {
77 instructions: Some(instructions.into()),
78 criteria: IndexMap::new(),
79 }
80 }
81
82 pub fn from_labels<I, S>(instructions: impl Into<Value>, labels: I) -> Self
84 where
85 I: IntoIterator<Item = S>,
86 S: Into<String>,
87 {
88 let mut c = Self::new(instructions);
89 c.criteria
90 .extend(labels.into_iter().map(|l| (l.into(), None)));
91 c
92 }
93
94 #[must_use]
96 pub fn option(mut self, label: impl Into<String>, description: impl Into<Value>) -> Self {
97 self.criteria.insert(label.into(), Some(description.into()));
98 self
99 }
100
101 #[must_use]
103 pub fn label(mut self, label: impl Into<String>) -> Self {
104 self.criteria.insert(label.into(), None);
105 self
106 }
107}
108
109#[derive(Debug, Clone, Default, PartialEq, Serialize)]
111#[non_exhaustive]
112#[serde(tag = "type", rename = "score")]
113pub struct Score {
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub instructions: Option<Value>,
117 pub criteria: Vec<Value>,
119}
120
121impl Score {
122 pub fn new<I, V>(instructions: impl Into<Value>, levels: I) -> Self
124 where
125 I: IntoIterator<Item = V>,
126 V: Into<Value>,
127 {
128 Self {
129 instructions: Some(instructions.into()),
130 criteria: levels.into_iter().map(Into::into).collect(),
131 }
132 }
133
134 #[must_use]
136 pub fn level(mut self, description: impl Into<Value>) -> Self {
137 self.criteria.push(description.into());
138 self
139 }
140}
141
142#[derive(Debug, Clone, PartialEq)]
145#[non_exhaustive]
146pub enum Question {
147 Noul(Noul),
149 Choice(Choice),
151 Score(Score),
153 Raw(Value),
155}
156
157impl Serialize for Question {
158 fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
159 match self {
160 Question::Noul(q) => q.serialize(s),
161 Question::Choice(q) => q.serialize(s),
162 Question::Score(q) => q.serialize(s),
163 Question::Raw(v) => v.serialize(s),
164 }
165 }
166}
167
168impl From<Noul> for Question {
169 fn from(q: Noul) -> Self {
170 Question::Noul(q)
171 }
172}
173impl From<Choice> for Question {
174 fn from(q: Choice) -> Self {
175 Question::Choice(q)
176 }
177}
178impl From<Score> for Question {
179 fn from(q: Score) -> Self {
180 Question::Score(q)
181 }
182}
183impl From<Value> for Question {
184 fn from(v: Value) -> Self {
185 Question::Raw(v)
186 }
187}
188
189#[derive(Debug, Clone, Default, PartialEq, Serialize)]
191#[serde(transparent)]
192pub struct Questions(IndexMap<String, Question>);
193
194impl Questions {
195 pub fn new() -> Self {
197 Self::default()
198 }
199
200 #[must_use]
202 pub fn with(mut self, name: impl Into<String>, question: impl Into<Question>) -> Self {
203 self.insert(name, question);
204 self
205 }
206
207 pub fn insert(
210 &mut self,
211 name: impl Into<String>,
212 question: impl Into<Question>,
213 ) -> Option<Question> {
214 self.0.insert(name.into(), question.into())
215 }
216
217 pub fn get(&self, name: &str) -> Option<&Question> {
219 self.0.get(name)
220 }
221
222 pub fn len(&self) -> usize {
224 self.0.len()
225 }
226
227 pub fn is_empty(&self) -> bool {
229 self.0.is_empty()
230 }
231
232 pub fn iter(&self) -> impl Iterator<Item = (&str, &Question)> {
234 self.0.iter().map(|(k, v)| (k.as_str(), v))
235 }
236
237 pub(crate) fn validate(&self) -> Result<()> {
239 if self.0.is_empty() {
240 return Err(Error::invalid_request("at least one question is required"));
241 }
242 for (name, q) in &self.0 {
243 match q {
244 Question::Score(s) if s.criteria.is_empty() => return Err(empty_score(name)),
245 Question::Choice(c) if c.criteria.is_empty() => return Err(empty_choice(name)),
246 Question::Raw(v) => validate_raw(name, v)?,
247 _ => {}
248 }
249 }
250 Ok(())
251 }
252}
253
254fn empty_score(name: &str) -> Error {
255 Error::invalid_request(format!(
256 "score question \"{name}\" has no criteria; at least one score is required"
257 ))
258}
259
260fn empty_choice(name: &str) -> Error {
261 Error::invalid_request(format!(
262 "choice question \"{name}\" has no criteria; at least one option is required"
263 ))
264}
265
266fn validate_raw(name: &str, v: &Value) -> Result<()> {
267 let ty = v
268 .as_object()
269 .and_then(|o| o.get("type"))
270 .and_then(Value::as_str)
271 .filter(|t| !t.is_empty())
272 .ok_or_else(|| {
273 Error::invalid_request(format!(
274 "question \"{name}\" must be a question object or a JSON object with a nonempty string \"type\""
275 ))
276 })?;
277 if matches!(ty, "choice" | "score") {
278 let criteria = v.get("criteria").ok_or_else(|| {
279 Error::invalid_request(format!("question \"{name}\" requires \"criteria\""))
280 })?;
281 let empty = match criteria {
282 Value::Null => true,
283 Value::Bool(b) => !b,
284 Value::String(s) => s.is_empty(),
285 Value::Array(a) => a.is_empty(),
286 Value::Object(o) => o.is_empty(),
287 Value::Number(n) => n.as_f64() == Some(0.0),
288 };
289 if empty {
290 return Err(if ty == "score" {
291 empty_score(name)
292 } else {
293 empty_choice(name)
294 });
295 }
296 }
297 Ok(())
298}
299
300impl<K: Into<String>, Q: Into<Question>> FromIterator<(K, Q)> for Questions {
301 fn from_iter<T: IntoIterator<Item = (K, Q)>>(iter: T) -> Self {
302 Self(
303 iter.into_iter()
304 .map(|(k, q)| (k.into(), q.into()))
305 .collect(),
306 )
307 }
308}
309
310impl<K: Into<String>, Q: Into<Question>> Extend<(K, Q)> for Questions {
311 fn extend<T: IntoIterator<Item = (K, Q)>>(&mut self, iter: T) {
313 self.0
314 .extend(iter.into_iter().map(|(k, q)| (k.into(), q.into())));
315 }
316}
317
318impl IntoIterator for Questions {
320 type Item = (String, Question);
321 type IntoIter = indexmap::map::IntoIter<String, Question>;
322
323 fn into_iter(self) -> Self::IntoIter {
324 self.0.into_iter()
325 }
326}
327
328impl<'a> IntoIterator for &'a Questions {
330 type Item = (&'a str, &'a Question);
331 type IntoIter = std::iter::Map<
332 indexmap::map::Iter<'a, String, Question>,
333 fn((&'a String, &'a Question)) -> (&'a str, &'a Question),
334 >;
335
336 fn into_iter(self) -> Self::IntoIter {
337 self.0.iter().map(|(k, v)| (k.as_str(), v))
338 }
339}
340
341impl<K: Into<String>, Q: Into<Question>, const N: usize> From<[(K, Q); N]> for Questions {
342 fn from(arr: [(K, Q); N]) -> Self {
343 arr.into_iter().collect()
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use serde_json::json;
351
352 #[test]
353 fn serializes_like_the_api_reference() {
354 let q = Questions::new()
355 .with(
356 "department",
357 Choice::new("Which team should handle this")
358 .option("billing", "Payment or subscription issues")
359 .label("other"),
360 )
361 .with(
362 "frustration",
363 Score::new("How frustrated", ["Calm", "Angry"]),
364 )
365 .with(
366 "is_urgent",
367 Noul::new("Urgent?").when_true("Explicitly time-sensitive"),
368 )
369 .with("bare", Noul::default());
370 assert_eq!(
371 serde_json::to_value(&q).unwrap(),
372 json!({
373 "department": {"type": "choice", "instructions": "Which team should handle this",
374 "criteria": {"billing": "Payment or subscription issues", "other": null}},
375 "frustration": {"type": "score", "instructions": "How frustrated", "criteria": ["Calm", "Angry"]},
376 "is_urgent": {"type": "noul", "instructions": "Urgent?", "criteria": {"true": "Explicitly time-sensitive"}},
377 "bare": {"type": "noul"}
378 })
379 );
380 let keys: Vec<_> = q.iter().map(|(k, _)| k).collect();
382 assert_eq!(keys, ["department", "frustration", "is_urgent", "bare"]);
383 }
384
385 #[test]
386 fn lookup_iteration_and_extend() {
387 let mut q: Questions = [("a", Noul::new("a"))].into_iter().collect();
388 q.extend([("b", Question::from(Noul::new("b")))]);
389 q.extend(vec![(String::from("a"), Score::new("a", ["lo", "hi"]))]);
390 assert_eq!(q.len(), 2);
391 assert!(matches!(q.get("a"), Some(Question::Score(_))));
392 assert_eq!(q.get("b"), Some(&Question::Noul(Noul::new("b"))));
393 assert_eq!(q.get("c"), None);
394
395 let borrowed: Vec<&str> = (&q).into_iter().map(|(k, _)| k).collect();
396 assert_eq!(borrowed, ["a", "b"]);
397 let mut names = Vec::new();
398 for (name, _) in &q {
399 names.push(name);
400 }
401 assert_eq!(names, borrowed);
402 let owned: Vec<String> = q.clone().into_iter().map(|(k, _)| k).collect();
403 assert_eq!(owned, ["a", "b"]);
404 assert_eq!(q.clone().into_iter().collect::<Questions>(), q);
406
407 let mut r = Questions::new();
409 assert_eq!(r.insert("c", Noul::new("c")), None);
410 assert_eq!(r.insert("c", Noul::new("d")), Some(Noul::new("c").into()));
411 assert_eq!(r.get("c"), Some(&Question::Noul(Noul::new("d"))));
412 }
413
414 #[test]
415 fn structured_instructions() {
416 let q = Score::new(
417 json!({"task": "rate", "focus": ["tone"]}),
418 [json!({"level": "low"}), json!("high")],
419 );
420 assert_eq!(
421 serde_json::to_value(Question::from(q)).unwrap(),
422 json!({"type": "score", "instructions": {"task": "rate", "focus": ["tone"]},
423 "criteria": [{"level": "low"}, "high"]})
424 );
425 }
426
427 #[test]
428 fn validation() {
429 assert!(Questions::new().validate().is_err());
430 assert!(
431 Questions::from([("s", Score::new("x", Vec::<Value>::new()))])
432 .validate()
433 .is_err()
434 );
435 assert!(
436 Questions::from([("r", json!({"instructions": "x"}))])
437 .validate()
438 .is_err()
439 );
440 assert!(
441 Questions::from([("r", json!({"type": ""}))])
442 .validate()
443 .is_err()
444 );
445 assert!(
446 Questions::from([("c", Choice::new("x"))])
447 .validate()
448 .is_err()
449 );
450 assert!(
451 Questions::from([("r", json!({"type": "choice"}))])
452 .validate()
453 .is_err()
454 );
455 assert!(
456 Questions::from([("r", json!({"type": "choice", "criteria": {}}))])
457 .validate()
458 .is_err()
459 );
460 assert!(
461 Questions::from([("r", json!({"type": "score", "criteria": []}))])
462 .validate()
463 .is_err()
464 );
465 assert!(
466 Questions::from([("r", json!({"type": "noul", "future_field": 1}))])
467 .validate()
468 .is_ok()
469 );
470 assert!(
471 Questions::from([("r", json!({"type": "choice", "criteria": {"a": null}}))])
472 .validate()
473 .is_ok()
474 );
475 }
476}