cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
/// CLI serialisation view for [`crate::domain::model::issue::Issue`].
use std::collections::BTreeMap;

use serde::Serialize;

use crate::domain::model::issue::Issue;
use crate::domain::model::status::RollupHistogram;
use crate::domain::usecases::issue::TagRollupValue;

/// A serialisable view of an issue for structured CLI output.
#[derive(Serialize)]
pub struct IssueView<'a> {
    pub id: String,
    pub title: &'a str,
    pub status: &'a str,
    pub date: String,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub tags: Vec<&'a str>,
    /// Parent issue id derived from the workspace.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent: Option<String>,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub children: Vec<String>,
    /// Status rollup over the direct children. Absent for issues
    /// without children.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rollup: Option<RollupView>,
    pub content: &'a str,
    pub events: EventLogView<'a>,
}

/// Structured shape of a composite issue's rollups: the derived
/// status block plus per-tag entries. Both projections are exposed
/// so downstream consumers (jq, scripts) pick what they need
/// without re-deriving.
#[derive(Serialize, Default)]
pub struct RollupView {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<StatusRollupView>,
    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
    pub tags: BTreeMap<String, TagRollupValue>,
}

#[derive(Serialize)]
pub struct StatusRollupView {
    pub category: String,
    pub histogram: RollupHistogram,
}

impl RollupView {
    pub fn from_status(h: RollupHistogram) -> Self {
        RollupView {
            status: Some(StatusRollupView {
                category: h.category().as_str().to_string(),
                histogram: h,
            }),
            tags: BTreeMap::new(),
        }
    }

    pub fn with_tag_rollups(mut self, tags: BTreeMap<String, TagRollupValue>) -> Self {
        self.tags = tags;
        self
    }
}

/// A serialisable view of an event log for structured CLI output.
#[derive(Serialize)]
pub struct EventLogView<'a>(Vec<EventView<'a>>);

/// A serialisable view of a single event.
#[derive(Serialize)]
pub struct EventView<'a> {
    pub timestamp: &'a str,
    pub action: &'a str,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub from: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub to: Option<String>,
}

impl<'a> IssueView<'a> {
    pub fn from_issue(issue: &'a Issue) -> Self {
        use crate::domain::model::event::EventAction;
        let events = EventLogView(
            issue
                .events
                .iter()
                .map(|e| {
                    let (from, to) = match &e.action {
                        EventAction::Created { state } => (None, Some(state.as_str().to_string())),
                        EventAction::StatusChanged { from, to } => (
                            Some(from.as_str().to_string()),
                            Some(to.as_str().to_string()),
                        ),
                    };
                    EventView {
                        timestamp: e.timestamp.as_str(),
                        action: e.action.as_str(),
                        from,
                        to,
                    }
                })
                .collect(),
        );

        IssueView {
            id: issue.id.to_string(),
            title: issue.title.as_str(),
            status: issue.status.as_str(),
            date: issue.date.to_string(),
            tags: issue.tags.iter().map(|t| t.as_str()).collect(),
            parent: None,
            children: Vec::new(),
            rollup: None,
            content: issue.content.as_str(),
            events,
        }
    }

    pub fn with_family(mut self, family: &crate::domain::usecases::issue::IssueFamily) -> Self {
        self.parent = family.parent.as_ref().map(|r| r.to_string());
        self.children = family.children.iter().map(|r| r.to_string()).collect();
        let mut view = match family.rollup {
            Some(h) => RollupView::from_status(h),
            None if family.tag_rollups.is_empty() => return self,
            None => RollupView::default(),
        };
        view = view.with_tag_rollups(family.tag_rollups.clone());
        self.rollup = Some(view);
        self
    }

    pub fn with_rollups(
        mut self,
        status: Option<RollupHistogram>,
        tags: BTreeMap<String, TagRollupValue>,
    ) -> Self {
        if status.is_none() && tags.is_empty() {
            return self;
        }
        let mut view = match status {
            Some(h) => RollupView::from_status(h),
            None => RollupView::default(),
        };
        view = view.with_tag_rollups(tags);
        self.rollup = Some(view);
        self
    }
}