use serde::Deserialize;
use super::cycle::Cycle;
use super::ids::*;
use super::page::{Connection, Ref};
use super::project::Project;
use super::team::User;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Priority {
#[default]
None,
Urgent,
High,
Medium,
Low,
}
impl Priority {
pub const ALL: [Priority; 5] = [
Self::None,
Self::Urgent,
Self::High,
Self::Medium,
Self::Low,
];
pub fn label(&self) -> &'static str {
match self {
Self::None => "None",
Self::Urgent => "Urgent",
Self::High => "High",
Self::Medium => "Medium",
Self::Low => "Low",
}
}
pub fn as_u8(self) -> u8 {
match self {
Self::None => 0,
Self::Urgent => 1,
Self::High => 2,
Self::Medium => 3,
Self::Low => 4,
}
}
pub fn as_index(self) -> usize {
self.as_u8() as usize
}
pub fn from_index(index: usize) -> Self {
match index {
1 => Self::Urgent,
2 => Self::High,
3 => Self::Medium,
4 => Self::Low,
_ => Self::None,
}
}
}
impl Priority {
fn from_api(value: f64) -> Self {
match value {
1.0 => Self::Urgent,
2.0 => Self::High,
3.0 => Self::Medium,
4.0 => Self::Low,
v => {
if v != 0.0 {
tracing::debug!(priority = v, "unrecognised priority");
}
Self::None
}
}
}
}
impl<'de> Deserialize<'de> for Priority {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let v = f64::deserialize(deserializer)?;
Ok(Self::from_api(v))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StateType {
Triage,
Backlog,
Unstarted,
Started,
Completed,
Cancelled,
Duplicate,
Unknown,
}
impl<'de> Deserialize<'de> for StateType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
Ok(match raw.as_str() {
"triage" => Self::Triage,
"backlog" => Self::Backlog,
"unstarted" => Self::Unstarted,
"started" => Self::Started,
"completed" => Self::Completed,
"canceled" | "cancelled" => Self::Cancelled,
"duplicate" => Self::Duplicate,
other => {
tracing::debug!(state_type = %other, "unrecognised workflow state type");
Self::Unknown
}
})
}
}
impl StateType {
pub fn as_str(self) -> &'static str {
match self {
Self::Triage => "triage",
Self::Backlog => "backlog",
Self::Unstarted => "unstarted",
Self::Started => "started",
Self::Completed => "completed",
Self::Cancelled => "canceled",
Self::Duplicate => "duplicate",
Self::Unknown => "unknown",
}
}
pub fn rank(&self) -> u8 {
match self {
Self::Triage => 0,
Self::Started => 1,
Self::Unstarted => 2,
Self::Backlog => 3,
Self::Completed => 4,
Self::Cancelled | Self::Duplicate => 5,
Self::Unknown => 6,
}
}
pub fn is_active(&self) -> bool {
matches!(self, Self::Started | Self::Unstarted)
}
pub fn is_backlog(&self) -> bool {
matches!(self, Self::Backlog | Self::Triage)
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Issue {
pub id: IssueId,
pub identifier: String,
pub title: String,
#[serde(default)]
pub priority: Priority,
#[serde(default, rename = "priorityLabel")]
pub priority_label: Option<String>,
pub state: Option<WorkflowState>,
pub assignee: Option<User>,
#[serde(default)]
pub labels: Option<Connection<Label>>,
pub description: Option<String>,
#[serde(default, rename = "createdAt")]
pub created_at: Option<String>,
#[serde(default, rename = "updatedAt")]
pub updated_at: Option<String>,
pub comments: Option<Connection<Comment>>,
pub project: Option<Project>,
#[serde(default, rename = "projectMilestone")]
pub project_milestone: Option<Milestone>,
pub cycle: Option<Cycle>,
#[serde(default)]
pub creator: Option<User>,
#[serde(default)]
pub estimate: Option<f64>,
#[serde(default, rename = "dueDate")]
pub due_date: Option<String>,
#[serde(default)]
pub parent: Option<IssueRef>,
#[serde(default)]
pub children: Option<Connection<IssueRef>>,
#[serde(default)]
pub url: Option<String>,
#[serde(default, rename = "branchName")]
pub branch_name: Option<String>,
#[serde(default)]
pub team: Option<Ref<TeamId>>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct IssueRef {
pub id: IssueId,
pub identifier: String,
pub title: String,
#[serde(default)]
pub state: Option<WorkflowState>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct Milestone {
pub id: MilestoneId,
pub name: String,
#[serde(default, rename = "targetDate")]
pub target_date: Option<String>,
#[serde(default)]
pub description: Option<String>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct WorkflowState {
pub id: WorkflowStateId,
pub name: String,
#[serde(default)]
pub color: Option<String>,
#[serde(default, rename = "type")]
pub state_type: Option<StateType>,
#[serde(default)]
pub position: Option<f64>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct Label {
pub id: LabelId,
pub name: String,
#[serde(default)]
pub color: Option<String>,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct Comment {
pub id: CommentId,
pub body: String,
#[serde(default, rename = "createdAt")]
pub created_at: Option<String>,
#[serde(default, rename = "editedAt")]
pub edited_at: Option<String>,
pub user: Option<User>,
#[serde(default)]
pub parent: Option<Ref<CommentId>>,
#[serde(default)]
pub url: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct IssueFilter {
pub status: Option<String>,
pub priority: Option<Priority>,
}
impl IssueFilter {
pub fn is_active(&self) -> bool {
self.status.is_some() || self.priority.is_some()
}
pub fn clear(&mut self) {
*self = Self::default();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_priority_and_its_place_in_the_menu_map_both_ways() {
for (index, priority) in Priority::ALL.into_iter().enumerate() {
assert_eq!(priority.as_index(), index);
assert_eq!(Priority::from_index(index), priority);
}
assert_eq!(Priority::from_index(9), Priority::None);
}
#[test]
fn each_category_reads_back_from_its_name() {
for kind in [
StateType::Triage,
StateType::Backlog,
StateType::Unstarted,
StateType::Started,
StateType::Completed,
StateType::Cancelled,
StateType::Duplicate,
StateType::Unknown,
] {
let read: StateType =
serde_json::from_value(serde_json::Value::from(kind.as_str())).unwrap();
assert_eq!(read, kind);
}
}
#[test]
fn lists_group_triage_first_and_unknown_last() {
let order = [
StateType::Triage,
StateType::Started,
StateType::Unstarted,
StateType::Backlog,
StateType::Completed,
StateType::Cancelled,
StateType::Unknown,
];
assert!(order.windows(2).all(|w| w[0].rank() < w[1].rank()));
assert_eq!(StateType::Duplicate.rank(), StateType::Cancelled.rank());
}
}