use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum Question {
YesNo(YesNoQuestion),
Choice(ChoiceQuestion),
Score(ScoreQuestion),
}
impl Question {
#[must_use]
pub fn yes_no(instructions: impl Into<String>) -> YesNoQuestion {
YesNoQuestion::new(instructions)
}
#[must_use]
pub fn choice(instructions: impl Into<String>) -> ChoiceQuestion {
ChoiceQuestion::new(instructions)
}
#[must_use]
pub fn score(instructions: impl Into<String>) -> ScoreQuestion {
ScoreQuestion::new(instructions)
}
#[must_use]
pub fn kind(&self) -> QuestionKind {
match self {
Self::YesNo(_) => QuestionKind::YesNo,
Self::Choice(_) => QuestionKind::Choice,
Self::Score(_) => QuestionKind::Score,
}
}
#[must_use]
pub fn instructions(&self) -> &str {
match self {
Self::YesNo(q) => &q.instructions,
Self::Choice(q) => &q.instructions,
Self::Score(q) => &q.instructions,
}
}
}
impl From<YesNoQuestion> for Question {
fn from(question: YesNoQuestion) -> Self {
Self::YesNo(question)
}
}
impl From<ChoiceQuestion> for Question {
fn from(question: ChoiceQuestion) -> Self {
Self::Choice(question)
}
}
impl From<ScoreQuestion> for Question {
fn from(question: ScoreQuestion) -> Self {
Self::Score(question)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum QuestionKind {
YesNo,
Choice,
Score,
}
impl QuestionKind {
pub const ALL: &[Self] = &[Self::YesNo, Self::Choice, Self::Score];
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::YesNo => "yes/no",
Self::Choice => "choice",
Self::Score => "score",
}
}
}
impl fmt::Display for QuestionKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct YesNoQuestion {
pub instructions: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub yes: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub no: Option<String>,
}
impl YesNoQuestion {
#[must_use]
pub fn new(instructions: impl Into<String>) -> Self {
Self {
instructions: instructions.into(),
yes: None,
no: None,
}
}
#[must_use]
pub fn yes(mut self, description: impl Into<String>) -> Self {
self.yes = Some(description.into());
self
}
#[must_use]
pub fn no(mut self, description: impl Into<String>) -> Self {
self.no = Some(description.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ChoiceQuestion {
pub instructions: String,
pub options: Vec<ChoiceOption>,
}
impl ChoiceQuestion {
#[must_use]
pub fn new(instructions: impl Into<String>) -> Self {
Self {
instructions: instructions.into(),
options: Vec::new(),
}
}
#[must_use]
pub fn option(mut self, key: impl Into<String>, description: impl Into<String>) -> Self {
self.options
.push(ChoiceOption::new(key).description(description));
self
}
#[must_use]
pub fn bare_option(mut self, key: impl Into<String>) -> Self {
self.options.push(ChoiceOption::new(key));
self
}
#[must_use]
pub fn options<I, S>(mut self, keys: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.options.extend(keys.into_iter().map(ChoiceOption::new));
self
}
#[must_use]
pub fn find_option(&self, key: &str) -> Option<&ChoiceOption> {
self.options.iter().find(|option| option.key == key)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ChoiceOption {
pub key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl ChoiceOption {
#[must_use]
pub fn new(key: impl Into<String>) -> Self {
Self {
key: key.into(),
description: None,
}
}
#[must_use]
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ScoreQuestion {
pub instructions: String,
pub levels: Vec<ScoreLevel>,
}
impl ScoreQuestion {
#[must_use]
pub fn new(instructions: impl Into<String>) -> Self {
Self {
instructions: instructions.into(),
levels: Vec::new(),
}
}
#[must_use]
pub fn level(mut self, label: impl Into<String>, description: impl Into<String>) -> Self {
self.levels
.push(ScoreLevel::new(label).description(description));
self
}
#[must_use]
pub fn bare_level(mut self, label: impl Into<String>) -> Self {
self.levels.push(ScoreLevel::new(label));
self
}
#[must_use]
pub fn levels<I, S>(mut self, labels: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.levels.extend(labels.into_iter().map(ScoreLevel::new));
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ScoreLevel {
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl ScoreLevel {
#[must_use]
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
description: None,
}
}
#[must_use]
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn serializes_tagged_by_kind() {
let question: Question = Question::choice("Pick")
.option("a", "First")
.bare_option("b")
.into();
assert_eq!(
serde_json::to_value(&question).unwrap(),
json!({
"type": "choice",
"instructions": "Pick",
"options": [{"key": "a", "description": "First"}, {"key": "b"}]
})
);
let yes_no: Question = Question::yes_no("Spam?").yes("It is spam").into();
assert_eq!(
serde_json::to_value(&yes_no).unwrap(),
json!({"type": "yes_no", "instructions": "Spam?", "yes": "It is spam"})
);
let back: Question =
serde_json::from_value(serde_json::to_value(&yes_no).unwrap()).unwrap();
assert_eq!(back, yes_no);
}
#[test]
fn score_levels_have_labels_and_descriptions() {
let score: Question = Question::score("Rate")
.level("low", "Barely")
.bare_level("high")
.into();
assert_eq!(
serde_json::to_value(&score).unwrap(),
json!({
"type": "score",
"instructions": "Rate",
"levels": [{"label": "low", "description": "Barely"}, {"label": "high"}]
})
);
assert_eq!(score.kind(), QuestionKind::Score);
assert_eq!(score.instructions(), "Rate");
assert_eq!(QuestionKind::YesNo.to_string(), "yes/no");
}
}