use serde::Serialize;
use serde_json::Value;
use typesafe::{Choice, Noul, Question, Questions, Score};
pub type Entry = (String, Question);
#[derive(Debug, Default)]
pub struct Session {
pub state: Value,
pub questions: Vec<Entry>,
pub model: Option<String>,
}
impl Session {
pub fn new() -> Self {
Self {
state: Value::String(String::new()),
questions: Vec::new(),
model: None,
}
}
pub fn state_is_empty(&self) -> bool {
match &self.state {
Value::Null => true,
Value::String(s) => s.trim().is_empty(),
Value::Array(a) => a.is_empty(),
Value::Object(o) => o.is_empty(),
_ => false,
}
}
pub fn state_preview(&self) -> String {
match &self.state {
Value::String(s) => s.clone(),
other => other.to_string(),
}
}
pub fn insert(&mut self, name: String, question: Question) -> bool {
if let Some(slot) = self.questions.iter_mut().find(|(n, _)| *n == name) {
slot.1 = question;
true
} else {
self.questions.push((name, question));
false
}
}
pub fn remove(&mut self, name: &str) -> bool {
let before = self.questions.len();
self.questions.retain(|(n, _)| n != name);
self.questions.len() != before
}
pub fn to_questions(&self) -> Questions {
self.questions.iter().cloned().collect()
}
pub fn request_json(&self, model: &str) -> String {
#[derive(Serialize)]
struct Body<'a> {
state: &'a Value,
model: &'a str,
questions: &'a Questions,
}
let questions = self.to_questions();
serde_json::to_string_pretty(&Body {
state: &self.state,
model,
questions: &questions,
})
.unwrap_or_else(|e| format!("<unencodable: {e}>"))
}
}
pub fn parse_noul(args: &str) -> Result<Entry, String> {
let (name, rest) = split_name(args, ":noul is_urgent The message conveys urgency")?;
let mut parts = rest.split('|').map(str::trim);
let instructions = parts.next().unwrap_or("");
if instructions.is_empty() {
return Err(
"A noul needs instructions: :noul is_urgent The message conveys urgency".into(),
);
}
let mut q = Noul::new(value(instructions));
for part in parts {
let (tag, text) = part
.split_once(':')
.map(|(t, v)| (t.trim().to_ascii_lowercase(), v.trim()))
.ok_or_else(|| format!("Expected `yes: …` or `no: …`, got {part:?}."))?;
match tag.as_str() {
"yes" | "true" => q = q.when_true(value(text)),
"no" | "false" => q = q.when_false(value(text)),
_ => return Err(format!("Unknown criterion {tag:?}; use `yes:` or `no:`.")),
}
}
Ok((name, q.into()))
}
pub fn parse_choice(args: &str) -> Result<Entry, String> {
let (name, rest) = split_name(
args,
":choice department Which team handles this | billing=Payments | technical=Bugs",
)?;
let mut parts = rest.split('|').map(str::trim);
let instructions = parts.next().unwrap_or("");
if instructions.is_empty() {
return Err("A choice needs instructions before the first `|`.".into());
}
let mut q = Choice::new(value(instructions));
let mut count = 0;
for part in parts.filter(|p| !p.is_empty()) {
q = match part.split_once('=') {
Some((label, desc)) => q.option(label.trim(), value(desc.trim())),
None => q.label(part),
};
count += 1;
}
if count < 2 {
return Err(
"A choice needs at least two options: … | billing=Payments | technical=Bugs".into(),
);
}
Ok((name, q.into()))
}
pub fn parse_score(args: &str) -> Result<Entry, String> {
let (name, rest) = split_name(
args,
":score frustration How frustrated they are | Calm | Annoyed | Furious",
)?;
let mut parts = rest.split('|').map(str::trim);
let instructions = parts.next().unwrap_or("");
if instructions.is_empty() {
return Err("A score needs instructions before the first `|`.".into());
}
let levels: Vec<Value> = parts.filter(|p| !p.is_empty()).map(value).collect();
if levels.len() < 2 {
return Err(
"A score needs at least two ordered levels: … | Calm | Annoyed | Furious".into(),
);
}
Ok((name, Score::new(value(instructions), levels).into()))
}
pub fn parse_raw(args: &str) -> Result<Entry, String> {
let (name, rest) = split_name(
args,
r#":raw tone {"type": "noul", "instructions": "Polite?"}"#,
)?;
let v: Value = serde_json::from_str(&rest).map_err(|e| format!("Not valid JSON: {e}"))?;
Ok((name, Question::Raw(v)))
}
fn split_name(args: &str, example: &str) -> Result<(String, String), String> {
let args = args.trim();
let (name, rest) = args
.split_once(char::is_whitespace)
.ok_or_else(|| format!("Missing name or body. Try: {example}"))?;
if name.is_empty() {
return Err(format!("Missing a question name. Try: {example}"));
}
Ok((name.to_owned(), rest.trim().to_owned()))
}
pub fn value(text: &str) -> Value {
let t = text.trim();
if (t.starts_with('{') || t.starts_with('['))
&& let Ok(v) = serde_json::from_str::<Value>(t)
{
return v;
}
Value::String(t.to_owned())
}
pub fn from_body(text: &str) -> Result<Session, String> {
let body: Value = serde_json::from_str(text).map_err(|e| format!("Not valid JSON: {e}"))?;
let obj = body.as_object().ok_or("Expected a JSON object.")?;
let questions = obj
.get("questions")
.and_then(Value::as_object)
.ok_or("Expected a `questions` object.")?;
Ok(Session {
state: obj.get("state").cloned().unwrap_or(Value::Null),
model: obj.get("model").and_then(Value::as_str).map(str::to_owned),
questions: questions
.iter()
.map(|(name, q)| (name.clone(), question_from_json(q)))
.collect(),
})
}
pub fn question_from_json(v: &Value) -> Question {
let instructions = v.get("instructions").cloned();
let criteria = v.get("criteria");
match v.get("type").and_then(Value::as_str) {
Some("noul") => {
let mut q = match instructions {
Some(i) => Noul::new(i),
None => Noul::default(),
};
if let Some(yes) = criteria.and_then(|c| c.get("true")) {
q = q.when_true(yes.clone());
}
if let Some(no) = criteria.and_then(|c| c.get("false")) {
q = q.when_false(no.clone());
}
q.into()
}
Some("choice") => {
let mut q = Choice::new(instructions.unwrap_or(Value::Null));
if let Some(map) = criteria.and_then(Value::as_object) {
for (label, desc) in map {
q = match desc {
Value::Null => q.label(label.clone()),
d => q.option(label.clone(), d.clone()),
};
}
}
q.into()
}
Some("score") => {
let levels = criteria
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
Score::new(instructions.unwrap_or(Value::Null), levels).into()
}
_ => Question::Raw(v.clone()),
}
}