use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::status::DeliverableStatus;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct DeliverableId(pub Uuid);
impl DeliverableId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
}
impl Default for DeliverableId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for DeliverableId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl std::str::FromStr for DeliverableId {
type Err = uuid::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(Uuid::parse_str(s)?))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DeliverableKind {
Feature,
Bugfix,
Refactor,
Chore,
Test,
Docs,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum EstimationTier {
S,
M,
L,
Xl,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Deliverable {
pub id: DeliverableId,
pub project_name: String,
pub name: String,
#[serde(default)]
pub description: String,
pub kind: DeliverableKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ticket_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spec_ref: Option<String>,
pub status: DeliverableStatus,
pub estimated_effort: EstimationTier,
pub created_at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_date: Option<DateTime<Utc>>,
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Deliverable {
Deliverable {
id: DeliverableId::new(),
project_name: "trusty-tools".into(),
name: "OAuth2 flow".into(),
description: "add oauth".into(),
kind: DeliverableKind::Feature,
ticket_ref: Some("#2117".into()),
spec_ref: Some("docs/specs/tm-project-control-plane.md".into()),
status: DeliverableStatus::Proposed,
estimated_effort: EstimationTier::L,
created_at: Utc::now(),
target_date: None,
}
}
#[test]
fn deliverable_serde_round_trip() {
let d = sample();
let json = serde_json::to_string(&d).unwrap();
let back: Deliverable = serde_json::from_str(&json).unwrap();
assert_eq!(d, back);
}
#[test]
fn deliverable_id_round_trip() {
let id = DeliverableId::new();
let s = id.to_string();
let parsed: DeliverableId = s.parse().unwrap();
assert_eq!(id, parsed);
assert_eq!(serde_json::to_string(&id).unwrap(), format!("\"{s}\""));
}
#[test]
fn estimation_tier_wire_format() {
assert_eq!(serde_json::to_string(&EstimationTier::S).unwrap(), "\"S\"");
assert_eq!(
serde_json::to_string(&EstimationTier::Xl).unwrap(),
"\"XL\""
);
let back: EstimationTier = serde_json::from_str("\"M\"").unwrap();
assert_eq!(back, EstimationTier::M);
}
#[test]
fn kind_wire_format() {
assert_eq!(
serde_json::to_string(&DeliverableKind::Feature).unwrap(),
"\"feature\""
);
assert_eq!(
serde_json::to_string(&DeliverableKind::Bugfix).unwrap(),
"\"bugfix\""
);
let back: DeliverableKind = serde_json::from_str("\"docs\"").unwrap();
assert_eq!(back, DeliverableKind::Docs);
}
#[test]
fn optional_fields_omitted_when_absent() {
let d = Deliverable {
ticket_ref: None,
spec_ref: None,
target_date: None,
..sample()
};
let json = serde_json::to_string(&d).unwrap();
assert!(!json.contains("ticket_ref"));
assert!(!json.contains("spec_ref"));
assert!(!json.contains("target_date"));
}
}