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