use crate::domain::model::body::Body;
use crate::domain::model::entry_locator::EntryLocator;
use crate::domain::model::entry_origin::EntryOrigin;
use crate::domain::model::issue::Issue;
use crate::domain::model::record_ref::IssueRef;
use crate::domain::model::status::Status;
use crate::domain::model::title::Title;
pub fn ir(n: u64) -> IssueRef {
IssueRef::new(format!("ISSUE-{n:04}")).unwrap()
}
pub fn st(s: &str) -> Status {
Status::new(s).unwrap()
}
pub fn make_issue(id: u64, title: &str, status: Status) -> Issue {
Issue {
id: ir(id),
title: Title::new(title).unwrap(),
description: None,
status,
date: crate::domain::model::temporal::iso_date::IsoDate::new("2026-01-01").unwrap(),
tags: crate::domain::model::tag_list::TagList::new(),
aliases: Vec::new(),
content: Body::default(),
events: crate::domain::model::issue::EventLog::new(),
links: crate::domain::model::issue::IssueLinks::new(),
relates: crate::domain::model::relates::Relates::default(),
assignee: None,
due_date: None,
tracker: crate::domain::model::issue::Tracker::local("ISSUE-0000"),
origin: EntryOrigin::Local,
location: EntryLocator::default(),
}
}
pub struct IssueFixture {
pub(crate) title: String,
pub(crate) kind: String,
pub(crate) status: String,
pub(crate) tags: Vec<String>,
pub(crate) id: Option<String>,
pub(crate) body: Option<String>,
pub(crate) links: Vec<(String, String)>,
pub(crate) relates_targets: Vec<String>,
pub(crate) events: Vec<(String, String, Option<String>, Option<String>)>,
pub(crate) date: Option<String>,
pub(crate) priority: Option<String>,
pub(crate) size: Option<String>,
}
pub fn issue(title: &str) -> IssueFixture {
IssueFixture {
title: title.to_string(),
kind: String::new(),
status: "open".to_string(),
tags: vec![],
id: None,
body: None,
links: vec![],
relates_targets: vec![],
events: vec![],
date: None,
priority: None,
size: None,
}
}
pub fn feature(title: &str) -> IssueFixture {
issue(title).kind("feature")
}
pub fn defect(title: &str) -> IssueFixture {
issue(title).kind("defect")
}
pub fn debt(title: &str) -> IssueFixture {
issue(title).kind("debt")
}
pub fn risk(title: &str) -> IssueFixture {
issue(title).kind("risk")
}
impl IssueFixture {
pub fn kind(mut self, kind: &str) -> Self {
self.kind = kind.to_string();
self
}
pub fn status(mut self, status: &str) -> Self {
self.status = status.to_string();
self
}
pub fn with_body(mut self, body: &str) -> Self {
self.body = Some(body.to_string());
self
}
pub fn with_link(mut self, target: &str, relationship: &str) -> Self {
self.links
.push((target.to_string(), relationship.to_string()));
self
}
pub fn with_relates(mut self, target: &str) -> Self {
self.relates_targets.push(target.to_string());
self
}
pub fn with_event(mut self, action: &str, from: Option<&str>, to: Option<&str>) -> Self {
self.events.push((
"2026-01-01T00:00:00Z".to_string(),
action.to_string(),
from.map(|s| s.to_string()),
to.map(|s| s.to_string()),
));
self
}
pub fn with_timestamped_event(
mut self,
timestamp: &str,
action: &str,
from: Option<&str>,
to: Option<&str>,
) -> Self {
self.events.push((
timestamp.to_string(),
action.to_string(),
from.map(|s| s.to_string()),
to.map(|s| s.to_string()),
));
self
}
pub fn date(mut self, date: &str) -> Self {
self.date = Some(date.to_string());
self
}
pub fn priority(mut self, priority: &str) -> Self {
self.priority = Some(priority.to_string());
self
}
pub fn size(mut self, size: &str) -> Self {
self.size = Some(size.to_string());
self
}
pub fn tags(mut self, tags: &str) -> Self {
self.tags = tags
.split(',')
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
.collect();
self
}
pub fn with_id(mut self, id: &str) -> Self {
self.id = Some(id.to_string());
self
}
pub fn build(self, auto_id: IssueRef) -> Issue {
use crate::domain::model::tag::Tag;
use crate::domain::model::tag_list::TagList;
let id = match self.id {
Some(ref s) => {
IssueRef::new(s).unwrap_or_else(|_| panic!("IssueFixture: invalid id {s:?}"))
}
None => auto_id,
};
let status = Status::new(&self.status)
.unwrap_or_else(|_| panic!("IssueFixture: invalid status {:?}", self.status));
let mut staged: Vec<Tag> = Vec::new();
if !self.kind.is_empty() {
let v = format!("flow:{}", self.kind);
staged.push(
Tag::new(&v)
.unwrap_or_else(|_| panic!("IssueFixture: invalid kind {:?}", self.kind)),
);
}
if let Some(ref p) = self.priority {
let v = format!("priority:{}", p.to_lowercase());
staged.push(
Tag::new(&v).unwrap_or_else(|_| panic!("IssueFixture: invalid priority {p:?}")),
);
}
if let Some(ref s) = self.size {
let v = format!("size:{}", s.to_lowercase());
staged
.push(Tag::new(&v).unwrap_or_else(|_| panic!("IssueFixture: invalid size {s:?}")));
}
for raw in &self.tags {
staged.push(
Tag::new(raw).unwrap_or_else(|_| panic!("IssueFixture: invalid tag {raw:?}")),
);
}
let tag_list: TagList = staged.into_iter().collect();
let content = self.body.as_deref().map(Body::new).unwrap_or_default();
let mut link_list = crate::domain::model::issue::IssueLinks::new();
for (target, relationship) in &self.links {
link_list.push(issue_link(target, relationship));
}
let event_log: crate::domain::model::event::EventLog = self
.events
.iter()
.map(|(timestamp, action, from, to)| {
use crate::domain::model::event::State;
use crate::domain::model::event::{Event, EventAction};
use crate::domain::model::temporal::timestamp::Timestamp;
let mk_state =
|s: &str| State::new(s).unwrap_or_else(|_| State::new("open").unwrap());
let action = match action.as_str() {
"created" => EventAction::Created {
state: mk_state(from.as_deref().unwrap_or("open")),
},
"status_changed" => EventAction::StatusChanged {
from: mk_state(from.as_deref().unwrap_or("open")),
to: mk_state(to.as_deref().unwrap_or("open")),
},
_ => EventAction::Created {
state: mk_state(from.as_deref().unwrap_or("open")),
},
};
Event {
timestamp: Timestamp::new(timestamp).unwrap_or_else(|_| {
panic!("IssueFixture: invalid timestamp {timestamp:?}")
}),
action,
}
})
.collect();
let date = self
.date
.as_deref()
.map(|d| {
crate::domain::model::temporal::iso_date::IsoDate::new(d)
.unwrap_or_else(|_| panic!("IssueFixture: invalid date {d:?}"))
})
.unwrap_or_else(|| {
crate::domain::model::temporal::iso_date::IsoDate::new("2026-01-01").unwrap()
});
Issue {
id,
title: Title::new(&self.title)
.unwrap_or_else(|_| panic!("IssueFixture: invalid title {:?}", self.title)),
description: None,
status,
date,
tags: tag_list,
aliases: Vec::new(),
content,
events: event_log,
links: link_list,
relates: self
.relates_targets
.iter()
.map(|s| {
crate::domain::model::entity_ref::EntityRef::new(s)
.unwrap_or_else(|_| panic!("IssueFixture: invalid relates target {s:?}"))
})
.collect(),
assignee: None,
due_date: None,
tracker: crate::domain::model::issue::Tracker::local("ISSUE-0000"),
origin: EntryOrigin::Local,
location: EntryLocator::default(),
}
}
}
pub fn issue_link(target: &str, relationship: &str) -> crate::domain::model::issue::IssueLink {
use crate::domain::model::issue::{IssueLink, IssueRelationship};
IssueLink {
target: IssueRef::new(target)
.unwrap_or_else(|_| panic!("issue_link: invalid target {target:?}")),
relationship: relationship
.parse::<IssueRelationship>()
.unwrap_or_else(|_| panic!("issue_link: unknown relationship {relationship:?}")),
}
}
pub fn status_changed_event(from: &str, to: &str) -> crate::domain::model::event::Event {
use crate::domain::model::event::{Event, EventAction, State};
use crate::domain::model::temporal::timestamp::Timestamp;
Event {
timestamp: Timestamp::new("2026-01-01T00:00:00Z").unwrap(),
action: EventAction::StatusChanged {
from: State::new(from).unwrap(),
to: State::new(to).unwrap(),
},
}
}
pub fn other_event() -> crate::domain::model::event::Event {
use crate::domain::model::event::{Event, EventAction, State};
use crate::domain::model::temporal::timestamp::Timestamp;
Event {
timestamp: Timestamp::new("2026-01-01T00:00:00Z").unwrap(),
action: EventAction::Created {
state: State::new("open").unwrap(),
},
}
}
pub fn enrich_issue(issue: &mut Issue, statuses: &crate::domain::model::status::StatusesConfig) {
if let Ok(resolved) = statuses.resolve(&issue.status.name.clone()) {
issue.status = resolved;
}
}
#[derive(Default)]
pub struct Filter {
pub(crate) status: Option<String>,
pub(crate) tags: Vec<String>,
}
impl Filter {
pub fn none() -> Self {
Self::default()
}
pub fn new() -> Self {
Self::default()
}
pub fn status(mut self, status: &str) -> Self {
self.status = Some(status.to_string());
self
}
pub fn kind(mut self, kind: &str) -> Self {
self.tags.push(format!("flow:{kind}"));
self
}
pub fn tags(mut self, tags: &str) -> Self {
for t in tags.split(',').map(|t| t.trim()).filter(|t| !t.is_empty()) {
self.tags.push(t.to_string());
}
self
}
pub fn resolved_status(&self) -> Option<Status> {
self.status
.as_deref()
.map(|s| Status::new(s).unwrap_or_else(|_| panic!("Filter: invalid status {s:?}")))
}
pub fn resolved_tag_filters(&self) -> Vec<crate::domain::model::tag_filter::TagFilter> {
use crate::domain::model::tag_filter::TagFilter;
self.tags
.iter()
.map(|t| TagFilter::parse(t).unwrap_or_else(|_| panic!("Filter: invalid tag {t:?}")))
.collect()
}
}