use serde::{Deserialize, Serialize};
use crate::error::{Result, TixError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum PatchValue<T> {
#[default]
Unchanged,
Set(T),
Clear,
}
impl<T> PatchValue<T> {
pub fn is_unchanged(&self) -> bool {
matches!(self, Self::Unchanged)
}
pub fn is_clear(&self) -> bool {
matches!(self, Self::Clear)
}
pub fn as_set(&self) -> Option<&T> {
match self {
Self::Set(v) => Some(v),
_ => None,
}
}
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> PatchValue<U> {
match self {
Self::Unchanged => PatchValue::Unchanged,
Self::Set(v) => PatchValue::Set(f(v)),
Self::Clear => PatchValue::Clear,
}
}
}
impl<T> From<Option<T>> for PatchValue<T> {
fn from(value: Option<T>) -> Self {
match value {
Some(v) => Self::Set(v),
None => Self::Unchanged,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RelationshipOp {
pub link: bool,
pub from: String,
pub to: String,
pub kind: crate::relationships::RelationKind,
}
impl RelationshipOp {
pub fn link(
from: impl Into<String>,
to: impl Into<String>,
kind: crate::relationships::RelationKind,
) -> Self {
Self {
link: true,
from: from.into(),
to: to.into(),
kind,
}
}
pub fn unlink(
from: impl Into<String>,
to: impl Into<String>,
kind: crate::relationships::RelationKind,
) -> Self {
Self {
link: false,
from: from.into(),
to: to.into(),
kind,
}
}
pub fn verb(&self) -> &'static str {
if self.link { "link" } else { "unlink" }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArchiveRequest {
pub archive: bool,
pub target_status: Option<String>,
pub reason: Option<String>,
}
impl ArchiveRequest {
pub fn archive() -> Self {
Self {
archive: true,
target_status: None,
reason: None,
}
}
pub fn restore(target_status: impl Into<String>, reason: Option<String>) -> Self {
let target = target_status.into();
Self {
archive: false,
target_status: (!target.trim().is_empty()).then_some(target),
reason,
}
}
pub fn problem(&self) -> Option<&'static str> {
if self.archive || self.target_status.is_some() {
None
} else {
Some("missing_target_status")
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct IssuePatch {
pub status: PatchValue<String>,
pub priority: PatchValue<crate::model::Priority>,
pub assignee: PatchValue<String>,
pub labels: PatchValue<Vec<String>>,
pub start_date: PatchValue<String>,
pub due_date: PatchValue<String>,
pub estimate: PatchValue<String>,
pub cycles: PatchValue<Vec<String>>,
pub milestones: PatchValue<Vec<String>>,
pub relationship: Option<RelationshipOp>,
pub archive: Option<ArchiveRequest>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PatchProblem {
pub field: &'static str,
pub message: String,
}
impl IssuePatch {
pub fn is_empty(&self) -> bool {
self.changed_fields().all(|(_, changed)| !changed)
&& self.relationship.is_none()
&& self.archive.is_none()
}
pub fn all_field_values(&self) -> [(&'static str, bool); 9] {
[
("status", !self.status.is_unchanged()),
("priority", !self.priority.is_unchanged()),
("assignee", !self.assignee.is_unchanged()),
("labels", !self.labels.is_unchanged()),
("start-date", !self.start_date.is_unchanged()),
("due-date", !self.due_date.is_unchanged()),
("estimate", !self.estimate.is_unchanged()),
("cycles", !self.cycles.is_unchanged()),
("milestones", !self.milestones.is_unchanged()),
]
}
pub fn changed_fields(&self) -> impl Iterator<Item = (&'static str, bool)> {
self.all_field_values()
.into_iter()
.filter(|(_, changed)| *changed)
}
pub fn has_any_field(&self) -> bool {
self.all_field_values().iter().any(|(_, changed)| *changed)
}
pub fn fields(&self) -> impl Iterator<Item = (&'static str, &PatchValue<String>)> {
[
("status", &self.status),
("assignee", &self.assignee),
("estimate", &self.estimate),
("start_date", &self.start_date),
("due_date", &self.due_date),
]
.into_iter()
}
pub fn field_names() -> &'static [&'static str] {
&[
"status",
"priority",
"assignee",
"labels",
"start-date",
"due-date",
"estimate",
"cycles",
"milestones",
]
}
pub fn structural_problems(&self) -> Vec<PatchProblem> {
let mut out = Vec::new();
if self.is_empty() {
out.push(PatchProblem {
field: "patch",
message: format!(
"no fields to change — name at least one of: {}",
Self::field_names().join(", ")
),
});
return out;
}
let kinds = [
self.relationship.is_some(),
self.archive.is_some(),
self.has_any_field(),
];
if kinds.iter().filter(|k| **k).count() > 1 {
let named: Vec<&str> = [
self.relationship
.as_ref()
.map(|_| "a relationship operation"),
self.archive.as_ref().map(|_| "an archive request"),
(!self.has_any_field())
.then_some("")
.filter(|s| s.is_empty())
.map(|_| "field changes"),
]
.into_iter()
.flatten()
.collect();
out.push(PatchProblem {
field: "patch",
message: format!(
"a patch is one of field changes, a relationship operation, or an \\
archive request — this one names {}",
named.join(" and ")
),
});
}
out
}
pub fn require_wellformed(&self) -> Result<()> {
match self.structural_problems().first() {
Some(problem) => Err(TixError::InvalidInput(problem.message.clone())),
None => Ok(()),
}
}
}
impl std::fmt::Display for PatchProblem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.field, self.message)
}
}