use std::collections::BTreeMap;
use serde::Serialize;
use serde_json::Value;
pub const NONE_OF_THE_ABOVE: &str = "none_of_the_above";
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
pub struct NoulCriteria {
#[serde(rename = "true", skip_serializing_if = "Option::is_none")]
pub yes: Option<String>,
#[serde(rename = "false", skip_serializing_if = "Option::is_none")]
pub no: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum Question {
Noul {
instructions: Value,
#[serde(skip_serializing_if = "Option::is_none")]
criteria: Option<NoulCriteria>,
},
Choice {
instructions: Value,
criteria: BTreeMap<String, Option<String>>,
},
Score { instructions: Value, criteria: Vec<String> },
}
impl Question {
pub fn noul(instructions: impl Into<Value>) -> Self {
Self::Noul {
instructions: instructions.into(),
criteria: None,
}
}
pub fn noul_with_criteria(instructions: impl Into<Value>, yes: impl Into<String>, no: impl Into<String>) -> Self {
Self::Noul {
instructions: instructions.into(),
criteria: Some(NoulCriteria {
yes: Some(yes.into()),
no: Some(no.into()),
}),
}
}
pub fn choice<K, V>(instructions: impl Into<Value>, options: impl IntoIterator<Item = (K, Option<V>)>) -> Self
where
K: Into<String>,
V: Into<String>,
{
Self::Choice {
instructions: instructions.into(),
criteria: options
.into_iter()
.map(|(k, v)| (k.into(), v.map(Into::into)))
.collect(),
}
}
pub fn choice_plain<K: Into<String>>(instructions: impl Into<Value>, options: impl IntoIterator<Item = K>) -> Self {
Self::choice(instructions, options.into_iter().map(|k| (k, None::<String>)))
}
pub fn score<L: Into<String>>(instructions: impl Into<Value>, levels: impl IntoIterator<Item = L>) -> Self {
Self::Score {
instructions: instructions.into(),
criteria: levels.into_iter().map(Into::into).collect(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn noul_serializes_without_criteria_when_absent() {
let q = Question::noul("Does this convey urgency?");
assert_eq!(
serde_json::to_value(&q).unwrap(),
json!({"type": "noul", "instructions": "Does this convey urgency?"})
);
}
#[test]
fn noul_criteria_use_true_false_keys() {
let q = Question::noul_with_criteria("Urgent?", "Explicitly time-sensitive", "No urgency expressed");
assert_eq!(
serde_json::to_value(&q).unwrap(),
json!({
"type": "noul",
"instructions": "Urgent?",
"criteria": {"true": "Explicitly time-sensitive", "false": "No urgency expressed"}
})
);
}
#[test]
fn choice_serializes_null_for_undescribed_options() {
let q = Question::choice("Which team?", [("billing", Some("Payments")), ("sales", None)]);
assert_eq!(
serde_json::to_value(&q).unwrap(),
json!({"type": "choice", "instructions": "Which team?", "criteria": {"billing": "Payments", "sales": null}})
);
}
#[test]
fn score_serializes_levels_in_order() {
let q = Question::score(json!({"rate": "frustration"}), ["Calm", "Frustrated", "Very angry"]);
assert_eq!(
serde_json::to_value(&q).unwrap(),
json!({"type": "score", "instructions": {"rate": "frustration"}, "criteria": ["Calm", "Frustrated", "Very angry"]})
);
}
}