1use serde::Serialize;
4use serde_json::Value;
5use typesafe::{Choice, Noul, Question, Questions, Score};
6
7pub type Entry = (String, Question);
9
10#[derive(Debug, Default, Clone)]
12pub struct Session {
13 pub state: Value,
15 pub questions: Vec<Entry>,
17 pub model: Option<String>,
19}
20
21impl Session {
22 pub fn new() -> Self {
23 Self {
24 state: Value::String(String::new()),
25 questions: Vec::new(),
26 model: None,
27 }
28 }
29
30 pub fn state_is_empty(&self) -> bool {
31 is_empty_value(&self.state)
32 }
33
34 pub fn state_preview(&self) -> String {
36 match &self.state {
37 Value::String(s) => s.clone(),
38 other => other.to_string(),
39 }
40 }
41
42 pub fn insert(&mut self, name: String, question: Question) -> bool {
44 if let Some(slot) = self.questions.iter_mut().find(|(n, _)| *n == name) {
45 slot.1 = question;
46 true
47 } else {
48 self.questions.push((name, question));
49 false
50 }
51 }
52
53 pub fn remove(&mut self, name: &str) -> bool {
54 let before = self.questions.len();
55 self.questions.retain(|(n, _)| n != name);
56 self.questions.len() != before
57 }
58
59 pub fn to_questions(&self) -> Questions {
60 self.questions.iter().cloned().collect()
61 }
62
63 pub fn request_json(&self, model: &str) -> String {
68 self.body_json(model, true)
69 }
70
71 pub fn request_json_compact(&self, model: &str) -> String {
74 self.body_json(model, false)
75 }
76
77 fn body_json(&self, model: &str, pretty: bool) -> String {
78 #[derive(Serialize)]
79 struct Body<'a> {
80 state: &'a Value,
81 model: &'a str,
82 questions: &'a Questions,
83 }
84 let questions = self.to_questions();
85 let body = Body {
86 state: &self.state,
87 model,
88 questions: &questions,
89 };
90 if pretty {
91 serde_json::to_string_pretty(&body)
92 } else {
93 serde_json::to_string(&body)
94 }
95 .unwrap_or_else(|e| format!("<unencodable: {e}>"))
96 }
97}
98
99pub fn is_empty_value(v: &Value) -> bool {
104 match v {
105 Value::Null => true,
106 Value::String(s) => s.trim().is_empty(),
107 Value::Array(a) => a.is_empty(),
108 Value::Object(o) => o.is_empty(),
109 _ => false,
110 }
111}
112
113pub fn parse_noul(args: &str) -> Result<Entry, String> {
115 let (name, rest) = split_name(args, ":noul is_urgent The message conveys urgency")?;
116 let mut parts = rest.split('|').map(str::trim);
117 let instructions = parts.next().unwrap_or("");
118 if instructions.is_empty() {
119 return Err(
120 "A noul needs instructions: :noul is_urgent The message conveys urgency".into(),
121 );
122 }
123 let mut q = Noul::new(value(instructions));
124 for part in parts {
125 let (tag, text) = part
126 .split_once(':')
127 .map(|(t, v)| (t.trim().to_ascii_lowercase(), v.trim()))
128 .ok_or_else(|| format!("Expected `yes: …` or `no: …`, got {part:?}."))?;
129 match tag.as_str() {
130 "yes" | "true" => q = q.when_true(value(text)),
131 "no" | "false" => q = q.when_false(value(text)),
132 _ => return Err(format!("Unknown criterion {tag:?}; use `yes:` or `no:`.")),
133 }
134 }
135 Ok((name, q.into()))
136}
137
138pub fn parse_choice(args: &str) -> Result<Entry, String> {
140 let (name, rest) = split_name(
141 args,
142 ":choice department Which team handles this | billing=Payments | technical=Bugs",
143 )?;
144 let mut parts = rest.split('|').map(str::trim);
145 let instructions = parts.next().unwrap_or("");
146 if instructions.is_empty() {
147 return Err("A choice needs instructions before the first `|`.".into());
148 }
149 let mut q = Choice::new(value(instructions));
150 let mut count = 0;
151 for part in parts.filter(|p| !p.is_empty()) {
152 q = match part.split_once('=') {
153 Some((label, desc)) => q.option(label.trim(), value(desc.trim())),
154 None => q.label(part),
155 };
156 count += 1;
157 }
158 if count < 2 {
159 return Err(
160 "A choice needs at least two options: … | billing=Payments | technical=Bugs".into(),
161 );
162 }
163 Ok((name, q.into()))
164}
165
166pub fn parse_score(args: &str) -> Result<Entry, String> {
168 let (name, rest) = split_name(
169 args,
170 ":score frustration How frustrated they are | Calm | Annoyed | Furious",
171 )?;
172 let mut parts = rest.split('|').map(str::trim);
173 let instructions = parts.next().unwrap_or("");
174 if instructions.is_empty() {
175 return Err("A score needs instructions before the first `|`.".into());
176 }
177 let levels: Vec<Value> = parts.filter(|p| !p.is_empty()).map(value).collect();
178 if levels.len() < 2 {
179 return Err(
180 "A score needs at least two ordered levels: … | Calm | Annoyed | Furious".into(),
181 );
182 }
183 Ok((name, Score::new(value(instructions), levels).into()))
184}
185
186pub fn parse_raw(args: &str) -> Result<Entry, String> {
188 let (name, rest) = split_name(
189 args,
190 r#":raw tone {"type": "noul", "instructions": "Polite?"}"#,
191 )?;
192 let v: Value = serde_json::from_str(&rest).map_err(|e| format!("Not valid JSON: {e}"))?;
193 Ok((name, Question::Raw(v)))
194}
195
196fn split_name(args: &str, example: &str) -> Result<(String, String), String> {
197 let args = args.trim();
198 let (name, rest) = args
199 .split_once(char::is_whitespace)
200 .ok_or_else(|| format!("Missing name or body. Try: {example}"))?;
201 if name.is_empty() {
202 return Err(format!("Missing a question name. Try: {example}"));
203 }
204 Ok((name.to_owned(), rest.trim().to_owned()))
205}
206
207pub fn value(text: &str) -> Value {
210 let t = text.trim();
211 if (t.starts_with('{') || t.starts_with('['))
212 && let Ok(v) = serde_json::from_str::<Value>(t)
213 {
214 return v;
215 }
216 Value::String(t.to_owned())
217}
218
219pub fn from_body(text: &str) -> Result<Session, String> {
222 let body: Value = serde_json::from_str(text).map_err(|e| format!("Not valid JSON: {e}"))?;
223 let obj = body.as_object().ok_or("Expected a JSON object.")?;
224 let questions = obj
225 .get("questions")
226 .and_then(Value::as_object)
227 .ok_or("Expected a `questions` object.")?;
228 Ok(Session {
229 state: obj.get("state").cloned().unwrap_or(Value::Null),
230 model: obj.get("model").and_then(Value::as_str).map(str::to_owned),
231 questions: questions
232 .iter()
233 .map(|(name, q)| (name.clone(), question_from_json(q)))
234 .collect(),
235 })
236}
237
238pub fn question_from_json(v: &Value) -> Question {
240 let instructions = v.get("instructions").cloned();
241 let criteria = v.get("criteria");
242 match v.get("type").and_then(Value::as_str) {
243 Some("noul") => {
244 let mut q = match instructions {
245 Some(i) => Noul::new(i),
246 None => Noul::default(),
247 };
248 if let Some(yes) = criteria.and_then(|c| c.get("true")) {
249 q = q.when_true(yes.clone());
250 }
251 if let Some(no) = criteria.and_then(|c| c.get("false")) {
252 q = q.when_false(no.clone());
253 }
254 q.into()
255 }
256 Some("choice") => {
257 let mut q = Choice::new(instructions.unwrap_or(Value::Null));
258 if let Some(map) = criteria.and_then(Value::as_object) {
259 for (label, desc) in map {
260 q = match desc {
261 Value::Null => q.label(label.clone()),
262 d => q.option(label.clone(), d.clone()),
263 };
264 }
265 }
266 q.into()
267 }
268 Some("score") => {
269 let levels = criteria
270 .and_then(Value::as_array)
271 .cloned()
272 .unwrap_or_default();
273 Score::new(instructions.unwrap_or(Value::Null), levels).into()
274 }
275 _ => Question::Raw(v.clone()),
276 }
277}