use std::collections::HashSet;
use fission::prelude::{TextContent, WidgetId};
use thiserror::Error;
#[derive(Clone, Debug, PartialEq)]
pub struct OnboardingStep {
pub id: String,
pub anchor: WidgetId,
pub route: Option<String>,
pub title: TextContent,
pub body: TextContent,
}
#[derive(Clone, Debug, PartialEq)]
pub struct OnboardingFlow {
id: String,
version: u32,
steps: Vec<OnboardingStep>,
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum FlowValidationError {
#[error("onboarding flow id cannot be empty")]
EmptyFlowId,
#[error("onboarding flow version must be at least one")]
InvalidVersion,
#[error("onboarding flow must contain at least one step")]
EmptySteps,
#[error("onboarding step id cannot be empty")]
EmptyStepId,
#[error("onboarding step id is duplicated: {0}")]
DuplicateStepId(String),
}
impl OnboardingFlow {
pub fn new(
id: impl Into<String>,
version: u32,
steps: Vec<OnboardingStep>,
) -> Result<Self, FlowValidationError> {
let id = id.into();
if id.trim().is_empty() {
return Err(FlowValidationError::EmptyFlowId);
}
if version == 0 {
return Err(FlowValidationError::InvalidVersion);
}
if steps.is_empty() {
return Err(FlowValidationError::EmptySteps);
}
let mut ids = HashSet::with_capacity(steps.len());
for step in &steps {
if step.id.trim().is_empty() {
return Err(FlowValidationError::EmptyStepId);
}
if !ids.insert(step.id.clone()) {
return Err(FlowValidationError::DuplicateStepId(step.id.clone()));
}
}
Ok(Self { id, version, steps })
}
pub fn id(&self) -> &str {
&self.id
}
pub fn version(&self) -> u32 {
self.version
}
pub fn steps(&self) -> &[OnboardingStep] {
&self.steps
}
pub fn step(&self, index: usize) -> Option<&OnboardingStep> {
self.steps.get(index)
}
pub fn step_index(&self, step_id: &str) -> Option<usize> {
self.steps.iter().position(|step| step.id == step_id)
}
pub fn is_last_step(&self, index: usize) -> bool {
index + 1 == self.steps.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn step(id: &str) -> OnboardingStep {
OnboardingStep {
id: id.into(),
anchor: WidgetId::explicit(id),
route: None,
title: TextContent::Key(format!("onboarding.{id}.title")),
body: TextContent::Key(format!("onboarding.{id}.body")),
}
}
#[test]
fn validates_and_indexes_versioned_flow() {
let flow = OnboardingFlow::new("first-task", 2, vec![step("ready"), step("create")])
.expect("valid flow");
assert_eq!(flow.id(), "first-task");
assert_eq!(flow.version(), 2);
assert_eq!(flow.step_index("create"), Some(1));
assert!(!flow.is_last_step(0));
assert!(flow.is_last_step(1));
}
#[test]
fn rejects_duplicate_step_ids() {
assert_eq!(
OnboardingFlow::new("first-task", 1, vec![step("create"), step("create")]),
Err(FlowValidationError::DuplicateStepId("create".into()))
);
}
}