use crate::domain::model::record_ref::IssueRef;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IssueRelationship {
Blocks,
BlockedBy,
ParentOf,
ChildOf,
}
impl IssueRelationship {
pub fn as_str(&self) -> &'static str {
match self {
IssueRelationship::Blocks => "blocks",
IssueRelationship::BlockedBy => "blocked-by",
IssueRelationship::ParentOf => "parent-of",
IssueRelationship::ChildOf => "child-of",
}
}
pub fn all() -> &'static [IssueRelationship] {
&[
IssueRelationship::Blocks,
IssueRelationship::BlockedBy,
IssueRelationship::ParentOf,
IssueRelationship::ChildOf,
]
}
pub fn user_writable() -> &'static [IssueRelationship] {
&[
IssueRelationship::Blocks,
IssueRelationship::BlockedBy,
IssueRelationship::ParentOf,
IssueRelationship::ChildOf,
]
}
pub fn is_hierarchical(&self) -> bool {
matches!(self, IssueRelationship::ParentOf)
}
pub fn inverse(&self) -> IssueRelationship {
match self {
IssueRelationship::Blocks => IssueRelationship::BlockedBy,
IssueRelationship::BlockedBy => IssueRelationship::Blocks,
IssueRelationship::ParentOf => IssueRelationship::ChildOf,
IssueRelationship::ChildOf => IssueRelationship::ParentOf,
}
}
}
impl std::str::FromStr for IssueRelationship {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"blocks" => Ok(IssueRelationship::Blocks),
"blocked-by" => Ok(IssueRelationship::BlockedBy),
"parent-of" => Ok(IssueRelationship::ParentOf),
"child-of" => Ok(IssueRelationship::ChildOf),
other => Err(anyhow::anyhow!(
"unknown issue relationship type: '{other}'"
)),
}
}
}
impl std::fmt::Display for IssueRelationship {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl serde::Serialize for IssueRelationship {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct IssueLink {
pub target: IssueRef,
pub relationship: IssueRelationship,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct IssueLinks(Vec<IssueLink>);
impl serde::Serialize for IssueLinks {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
self.0.serialize(s)
}
}
impl IssueLinks {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, link: IssueLink) {
self.0.push(link);
}
pub fn with_added(&self, link: IssueLink) -> Self {
let mut out = self.clone();
out.0.push(link);
out
}
pub fn with_removed(&self, link: &IssueLink) -> Option<Self> {
let pos = self
.0
.iter()
.position(|l| l.target == link.target && l.relationship == link.relationship)?;
let mut out = self.clone();
out.0.remove(pos);
Some(out)
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn iter(&self) -> impl Iterator<Item = &IssueLink> {
self.0.iter()
}
}
impl IntoIterator for IssueLinks {
type Item = IssueLink;
type IntoIter = std::vec::IntoIter<IssueLink>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a IssueLinks {
type Item = &'a IssueLink;
type IntoIter = std::slice::Iter<'a, IssueLink>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl FromIterator<IssueLink> for IssueLinks {
fn from_iter<I: IntoIterator<Item = IssueLink>>(iter: I) -> Self {
IssueLinks(iter.into_iter().collect())
}
}
impl std::ops::Index<usize> for IssueLinks {
type Output = IssueLink;
fn index(&self, i: usize) -> &Self::Output {
&self.0[i]
}
}
#[cfg(test)]
pub mod strategy {
use super::{IssueLink, IssueLinks, IssueRelationship};
use crate::domain::model::record_ref::strategy::issue_ref;
use proptest::prelude::*;
pub fn issue_relationship() -> impl Strategy<Value = IssueRelationship> {
prop_oneof![
Just(IssueRelationship::Blocks),
Just(IssueRelationship::BlockedBy),
Just(IssueRelationship::ParentOf),
Just(IssueRelationship::ChildOf),
]
}
prop_compose! {
pub fn issue_link()(
target in issue_ref(),
relationship in issue_relationship(),
) -> IssueLink {
IssueLink { target, relationship }
}
}
pub fn issue_links() -> impl Strategy<Value = IssueLinks> {
proptest::collection::vec(issue_link(), 0..=8).prop_map(|v| v.into_iter().collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_issue_link() -> IssueLink {
IssueLink {
target: IssueRef::new("ISSUE-0001").unwrap(),
relationship: IssueRelationship::Blocks,
}
}
#[test]
fn issue_relationship_as_str_roundtrips() {
for rel in IssueRelationship::all() {
let s = rel.as_str();
let parsed: IssueRelationship = s.parse().unwrap();
assert_eq!(&parsed, rel);
}
}
#[test]
fn issue_relationship_all_contains_all_variants() {
let all = IssueRelationship::all();
assert!(all.contains(&IssueRelationship::Blocks));
assert!(all.contains(&IssueRelationship::BlockedBy));
assert!(all.contains(&IssueRelationship::ParentOf));
assert!(all.contains(&IssueRelationship::ChildOf));
}
#[test]
fn all_variants_are_user_writable() {
let uw = IssueRelationship::user_writable();
assert_eq!(uw.len(), 4);
assert!(uw.contains(&IssueRelationship::Blocks));
assert!(uw.contains(&IssueRelationship::BlockedBy));
assert!(uw.contains(&IssueRelationship::ParentOf));
assert!(uw.contains(&IssueRelationship::ChildOf));
}
#[test]
fn inverses_are_mutual() {
for rel in IssueRelationship::all() {
assert_eq!(&rel.inverse().inverse(), rel);
}
assert_eq!(
IssueRelationship::Blocks.inverse(),
IssueRelationship::BlockedBy
);
assert_eq!(
IssueRelationship::ParentOf.inverse(),
IssueRelationship::ChildOf
);
}
#[test]
fn parent_of_is_the_only_hierarchical_relationship() {
assert!(IssueRelationship::ParentOf.is_hierarchical());
assert!(!IssueRelationship::Blocks.is_hierarchical());
}
#[test]
fn issue_relationship_from_str_rejects_unknown() {
assert!("unknown".parse::<IssueRelationship>().is_err());
}
#[test]
fn issue_relationship_display_matches_as_str() {
for rel in IssueRelationship::all() {
assert_eq!(rel.to_string(), rel.as_str());
}
}
#[test]
fn issue_links_new_is_empty() {
assert!(IssueLinks::new().is_empty());
}
#[test]
fn issue_links_push_makes_non_empty() {
let mut l = IssueLinks::new();
l.push(sample_issue_link());
assert!(!l.is_empty());
}
#[test]
fn issue_links_iter_yields_items() {
let mut l = IssueLinks::new();
l.push(sample_issue_link());
assert_eq!(l.iter().count(), 1);
}
#[test]
fn issue_links_into_iter_ref_works_in_for_loop() {
let mut l = IssueLinks::new();
l.push(sample_issue_link());
let mut count = 0;
for _ in &l {
count += 1;
}
assert_eq!(count, 1);
}
#[test]
fn issue_links_from_iter_collects() {
let items = vec![sample_issue_link()];
let l: IssueLinks = items.into_iter().collect();
assert_eq!(l.iter().count(), 1);
}
use super::strategy;
proptest::proptest! {
#[test]
fn prop_relationship_as_str_roundtrips(rel in strategy::issue_relationship()) {
proptest::prop_assert_eq!(rel.as_str().parse::<IssueRelationship>().unwrap(), rel);
}
#[test]
fn prop_link_preserves_fields(link in strategy::issue_link()) {
let cloned = link.clone();
proptest::prop_assert_eq!(&cloned.target, &link.target);
proptest::prop_assert_eq!(&cloned.relationship, &link.relationship);
}
#[test]
fn prop_links_len_matches_iter_count(links in strategy::issue_links()) {
proptest::prop_assert_eq!(links.len(), links.iter().count());
}
}
}