1use serde_json::Value;
9
10use crate::pyjson::Dumps;
11use crate::request::{Criteria, Question};
12
13const RAW: Dumps = Dumps { ensure_ascii: false };
14const ASCII: Dumps = Dumps { ensure_ascii: true };
15
16pub const NOUL_FALSE: &str = "no, the statement does not hold";
18pub const NOUL_TRUE: &str = "yes, the statement holds";
20
21#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct CompatText {
25 pub head: String,
27 pub options: Vec<String>,
29}
30
31#[must_use]
34pub fn compat_question(q: &Question, mask: &str) -> CompatText {
35 let ins = match &q.instructions {
39 None => String::new(),
40 Some(Value::String(s)) => s.clone(),
41 Some(v) => ASCII.to_string(v),
42 };
43 let head = format!("{} question: {}", q.qtype.as_str(), ins.replace(mask, " "));
44 let options = compat_options(&q.criteria)
45 .into_iter()
46 .map(|o| format!(" {}", o.replace(mask, " ")))
47 .collect();
48 CompatText { head, options }
49}
50
51#[must_use]
53pub fn compat_options(c: &Criteria) -> Vec<String> {
54 match c {
55 Criteria::Choice(opts) => opts
56 .iter()
57 .map(|o| match &o.description {
58 None => o.label.clone(),
60 Some(Value::String(s)) if s.is_empty() => o.label.clone(),
61 Some(v) => format!("{}: {}", o.label, criterion(v)),
62 })
63 .collect(),
64 Criteria::Score(levels) => {
65 levels.iter().enumerate().map(|(i, v)| format!("level {i}: {}", criterion(v))).collect()
66 }
67 Criteria::Noul { when_false, when_true } => {
68 let side = |v: &Option<Value>, default: &str| match v {
69 None | Some(Value::Null) => default.to_string(),
70 Some(Value::String(s)) if s.is_empty() => default.to_string(),
71 Some(v) => criterion(v),
72 };
73 vec![
74 format!("false: {}", side(when_false, NOUL_FALSE)),
75 format!("true: {}", side(when_true, NOUL_TRUE)),
76 ]
77 }
78 }
79}
80
81#[must_use]
83pub fn criterion(v: &Value) -> String {
84 match v {
85 Value::String(s) => s.clone(),
86 v => RAW.to_string(v),
87 }
88}
89
90#[must_use]
92pub fn compat_state(state: &Value, mask: &str) -> String {
93 let s = match state {
94 Value::String(s) => s.clone(),
95 v => RAW.to_string(v),
96 };
97 s.replace(mask, " ")
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::request::{Limits, parse};
104 use serde_json::json;
105
106 fn render(q: Value) -> CompatText {
107 let r = parse(&json!({"state": "", "questions": {"q": q}}), &Limits::LAYA).unwrap();
108 compat_question(&r.questions[0], "[MASK]")
109 }
110
111 #[test]
112 fn choice_score_noul() {
113 let t = render(
114 json!({"type": "choice", "instructions": "Pick [MASK] one", "criteria": {"a": "", "b": null, "c": 0, "d": {"x": [1, 2.5]}}}),
115 );
116 assert_eq!(t.head, "choice question: Pick one");
117 assert_eq!(t.options, [" a", " b", " c: 0", " d: {\"x\": [1, 2.5]}"]);
118 let t = render(
119 json!({"type": "score", "instructions": {"ask": "é"}, "criteria": ["low", null, true]}),
120 );
121 assert_eq!(t.head, "score question: {\"ask\": \"\\u00e9\"}");
122 assert_eq!(t.options, [" level 0: low", " level 1: null", " level 2: true"]);
123 let t = render(
124 json!({"type": "noul", "instructions": null, "criteria": {"TRUE": "", "false": "nope"}}),
125 );
126 assert_eq!(t.head, "noul question: null");
127 assert_eq!(t.options, [" false: nope", " true: yes, the statement holds"]);
128 }
129
130 #[test]
131 fn state() {
132 assert_eq!(
133 compat_state(&json!({"k": "ü", "n": [1e-5]}), "[MASK]"),
134 "{\"k\": \"ü\", \"n\": [1e-05]}"
135 );
136 assert_eq!(compat_state(&json!("a<mask>b"), "<mask>"), "a b");
137 }
138}