pub const ROADMAP_VERSION: &str = "1.0";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Roadmap {
pub roadmap_version: String,
#[serde(
default = "default_github_enabled",
deserialize_with = "deserialize_bool_lenient"
)]
pub github_enabled: bool,
pub github_repo: Option<String>,
#[serde(default)]
pub roadmap: Vec<RoadmapItem>,
}
fn default_github_enabled() -> bool {
true
}
fn deserialize_bool_lenient<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de;
struct BoolVisitor;
impl<'de> de::Visitor<'de> for BoolVisitor {
type Value = bool;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a boolean or string \"true\"/\"false\"")
}
fn visit_bool<E: de::Error>(self, v: bool) -> Result<bool, E> {
Ok(v)
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<bool, E> {
match v {
"true" => Ok(true),
"false" => Ok(false),
_ => Err(E::custom(format!("expected true/false, got '{}'", v))),
}
}
}
deserializer.deserialize_any(BoolVisitor)
}
fn default_timestamp() -> String {
"1970-01-01T00:00:00Z".to_string()
}
impl Default for Roadmap {
fn default() -> Self {
Self {
roadmap_version: ROADMAP_VERSION.to_string(),
github_enabled: true,
github_repo: None,
roadmap: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RoadmapItem {
pub id: String,
pub github_issue: Option<u64>,
#[serde(default = "default_item_type")]
pub item_type: ItemType,
pub title: String,
pub status: ItemStatus,
#[serde(default)]
pub priority: Priority,
pub assigned_to: Option<String>,
#[serde(default = "default_timestamp")]
pub created: String,
#[serde(default = "default_timestamp")]
pub updated: String,
pub spec: Option<PathBuf>,
#[serde(default)]
pub acceptance_criteria: Vec<String>,
#[serde(default, deserialize_with = "deserialize_phases")]
pub phases: Vec<Phase>,
#[serde(default)]
pub subtasks: Vec<Subtask>,
pub estimated_effort: Option<String>,
#[serde(default)]
pub labels: Vec<String>,
#[serde(default)]
pub notes: Option<String>,
}
fn default_item_type() -> ItemType {
ItemType::Task
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ItemType {
Task,
Epic,
Bug,
Feature,
Enhancement,
Documentation,
Refactor,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Default)]
#[serde(rename_all = "lowercase")]
pub enum Priority {
Low,
#[default]
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Phase {
pub name: String,
pub status: ItemStatus,
pub estimated_effort: Option<String>,
#[serde(default)]
pub completion: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Subtask {
pub id: String,
pub github_issue: Option<u64>,
pub title: String,
pub status: ItemStatus,
#[serde(default)]
pub completion: u8,
}
fn deserialize_phases<'de, D>(deserializer: D) -> Result<Vec<Phase>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{self, SeqAccess, Visitor};
use std::fmt;
struct PhasesVisitor;
impl<'de> Visitor<'de> for PhasesVisitor {
type Value = Vec<Phase>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a sequence of Phase structs")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut phases = Vec::new();
let mut index = 0;
while let Some(value) = seq.next_element::<serde_yaml_ng::Value>()? {
match value {
serde_yaml_ng::Value::String(s) => {
return Err(de::Error::custom(format!(
"phases[{}]: invalid type. \
Phases must be structs with 'name' and 'status' fields.\n\n\
Example:\n \
phases:\n \
- name: \"{}\"\n \
status: planned\n\n\
Found string: \"{}\"",
index, s, s
)));
}
serde_yaml_ng::Value::Mapping(_) => {
let phase: Phase =
serde_yaml_ng::from_value(value).map_err(de::Error::custom)?;
phases.push(phase);
}
_ => {
return Err(de::Error::custom(format!(
"phases[{}]: expected a Phase struct, found {:?}",
index, value
)));
}
}
index += 1;
}
Ok(phases)
}
}
deserializer.deserialize_seq(PhasesVisitor)
}