use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
pub const CUSTOM_RESPONSE_LABEL: &str = "Personalize your response";
pub const RECOMMENDED_SUFFIX: &str = "(Recommended)";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, JsonSchema)]
pub enum QuestionType {
#[default]
Text,
SingleChoice,
MultiChoice,
}
impl QuestionType {
pub fn is_choice(self) -> bool {
matches!(self, Self::SingleChoice | Self::MultiChoice)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct QuestionOption {
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl QuestionOption {
pub fn from_label(label: impl Into<String>) -> Self {
Self {
label: label.into(),
description: None,
}
}
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct QuestionItem {
pub question: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub header: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub options: Option<Vec<QuestionOption>>,
#[serde(
rename = "multiSelect",
default,
skip_serializing_if = "std::ops::Not::not"
)]
pub multi_select: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub purpose: Option<String>,
#[serde(default = "default_required")]
pub required: bool,
#[serde(default)]
#[schemars(skip)]
pub question_type: QuestionType,
#[serde(default, skip_serializing_if = "String::is_empty")]
#[schemars(skip)]
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(skip)]
pub recommended: Option<String>,
}
impl Default for QuestionItem {
fn default() -> Self {
Self {
question: String::new(),
header: None,
options: None,
multi_select: false,
purpose: None,
required: true,
question_type: QuestionType::Text,
id: String::new(),
recommended: None,
}
}
}
impl QuestionItem {
pub fn text(question: impl Into<String>) -> Self {
Self {
question: question.into(),
..Self::default()
}
}
pub fn single_choice(question: impl Into<String>, labels: &[&str]) -> Self {
Self {
question: question.into(),
options: Some(
labels
.iter()
.map(|l| QuestionOption::from_label(*l))
.collect(),
),
question_type: QuestionType::SingleChoice,
..Self::default()
}
}
pub fn multi_choice(question: impl Into<String>, labels: &[&str]) -> Self {
Self {
question: question.into(),
options: Some(
labels
.iter()
.map(|l| QuestionOption::from_label(*l))
.collect(),
),
question_type: QuestionType::MultiChoice,
multi_select: true,
..Self::default()
}
}
}
const fn default_required() -> bool {
true
}
fn question_item_from_wire(value: &JsonValue) -> Option<QuestionItem> {
let obj = value.as_object()?;
let string_field = |key: &str| {
obj.get(key)
.and_then(JsonValue::as_str)
.map(str::to_string)
.filter(|s| !s.is_empty())
};
let question = string_field("question").or_else(|| string_field("header"))?;
let header = string_field("header");
let multi_select = obj
.get("multiSelect")
.or_else(|| obj.get("multiple"))
.and_then(JsonValue::as_bool)
.unwrap_or(false);
let options = decode_options(obj.get("options"));
Some(QuestionItem {
question,
header,
options,
multi_select,
purpose: string_field("purpose"),
required: obj
.get("required")
.and_then(JsonValue::as_bool)
.unwrap_or(true),
question_type: QuestionType::Text,
id: String::new(),
recommended: None,
})
}
fn decode_options(value: Option<&JsonValue>) -> Option<Vec<QuestionOption>> {
let arr: &Vec<JsonValue> = match value? {
JsonValue::Array(arr) => arr,
JsonValue::String(s) => match serde_json::from_str::<Vec<JsonValue>>(s) {
Ok(parsed) => {
return Some(parsed.iter().filter_map(decode_option_item).collect());
}
Err(_) => return None,
},
_ => return None,
};
Some(arr.iter().filter_map(decode_option_item).collect())
}
fn decode_option_item(item: &JsonValue) -> Option<QuestionOption> {
match item {
JsonValue::String(label) => {
if label.trim().is_empty() {
None
} else {
Some(QuestionOption::from_label(label.clone()))
}
}
JsonValue::Object(_) => serde_json::from_value(item.clone())
.ok()
.filter(|o: &QuestionOption| !o.label.trim().is_empty()),
_ => None,
}
}
impl<'de> serde::de::Deserialize<'de> for QuestionItem {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let value = <JsonValue as serde::de::Deserialize>::deserialize(deserializer)?;
question_item_from_wire(&value).ok_or_else(|| {
serde::de::Error::custom(
"each question needs a non-empty `question` (or `header`) field",
)
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AnswerItem {
pub id: String,
pub answer: Option<String>,
pub selected: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct QuestionInput {
pub questions: Vec<QuestionItem>,
}
impl QuestionInput {
pub(crate) fn finalize(&mut self) {
let mut used: std::collections::HashSet<String> = self
.questions
.iter()
.filter(|q| !q.id.is_empty())
.map(|q| q.id.clone())
.collect();
for (idx, q) in self.questions.iter_mut().enumerate() {
if q.id.is_empty() {
let mut candidate = format!("q{idx}");
let mut n = 1;
while used.contains(&candidate) {
candidate = format!("q{idx}-{n}");
n += 1;
}
used.insert(candidate.clone());
q.id = candidate;
}
if q.multi_select {
q.question_type = QuestionType::MultiChoice;
} else if q.options.as_ref().is_some_and(|o| !o.is_empty()) {
q.question_type = QuestionType::SingleChoice;
} else {
q.question_type = QuestionType::Text;
}
if let Some(opts) = &mut q.options {
let mut found: Option<String> = None;
for opt in opts.iter_mut() {
if let Some(stripped) = opt
.label
.strip_suffix(RECOMMENDED_SUFFIX)
.map(str::trim_end)
.filter(|l| !l.is_empty())
{
opt.label = stripped.to_string();
found.get_or_insert_with(|| opt.label.clone());
}
}
if q.recommended.is_none() && q.question_type == QuestionType::SingleChoice {
q.recommended = found;
}
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct QuestionOutput {
pub questions: Vec<QuestionItem>,
pub answers: Vec<AnswerItem>,
}