use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, thiserror::Error)]
pub enum TrackerError {
#[error("Network error: {0}")]
Network(String),
#[error("Authentication failed: {0}")]
Auth(String),
#[error("Ticket not found: {0}")]
NotFound(String),
#[error("Not supported by this tracker: {0}")]
Unsupported(String),
#[error("{0}")]
Other(String),
}
pub const LINKED_TICKET_SCHEMA_VERSION: u32 = 2;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LinkedTicket {
#[serde(default)]
pub schema_version: u32,
pub id: String,
pub subject: String,
pub status: String,
pub url: String,
#[serde(default)]
pub author: Option<String>,
#[serde(default)]
pub assignee: Option<String>,
#[serde(default)]
pub time_estimate: Option<u32>,
#[serde(default)]
pub time_spent: Option<u32>,
#[serde(default)]
pub time_remaining: Option<u32>,
#[serde(default)]
pub tracker_type: Option<String>,
#[serde(default)]
pub priority: Option<String>,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub start_date: Option<String>,
#[serde(default)]
pub done_ratio: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Activity {
pub id: u32,
pub name: String,
}
#[derive(Debug, Clone)]
pub struct TimeEntry {
pub id: u64,
pub hours: f32,
pub activity: Activity,
pub comment: String,
pub user: String,
pub spent_on: String,
}
#[derive(Debug, Clone)]
pub struct TimeEntryRequest {
pub hours: f32,
pub activity_id: u32,
pub comment: String,
pub spent_on: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TicketChange {
Priority { old: String, new: String },
Status { old: String, new: String },
Assignee { old: String, new: String },
Version { old: String, new: String },
DoneRatio { old: String, new: String },
}
impl TicketChange {
pub fn field_label(&self) -> &'static str {
match self {
TicketChange::Priority { .. } => "priority",
TicketChange::Status { .. } => "status",
TicketChange::Assignee { .. } => "assignee",
TicketChange::Version { .. } => "version",
TicketChange::DoneRatio { .. } => "progress",
}
}
pub fn before_after(&self) -> (&str, &str) {
match self {
TicketChange::Priority { old, new }
| TicketChange::Status { old, new }
| TicketChange::Assignee { old, new }
| TicketChange::Version { old, new }
| TicketChange::DoneRatio { old, new } => (old.as_str(), new.as_str()),
}
}
}
impl LinkedTicket {
pub fn diff(&self, new: &LinkedTicket) -> Vec<TicketChange> {
let mut changes = Vec::new();
let opt_str =
|v: Option<&str>, fallback: &str| -> String { v.unwrap_or(fallback).to_string() };
let old_priority = opt_str(self.priority.as_deref(), "None");
let new_priority = opt_str(new.priority.as_deref(), "None");
if old_priority != new_priority {
changes.push(TicketChange::Priority {
old: old_priority,
new: new_priority,
});
}
if self.status != new.status {
changes.push(TicketChange::Status {
old: self.status.clone(),
new: new.status.clone(),
});
}
let old_assignee = opt_str(self.assignee.as_deref(), "Unassigned");
let new_assignee = opt_str(new.assignee.as_deref(), "Unassigned");
if old_assignee != new_assignee {
changes.push(TicketChange::Assignee {
old: old_assignee,
new: new_assignee,
});
}
let old_version = opt_str(self.version.as_deref(), "None");
let new_version = opt_str(new.version.as_deref(), "None");
if old_version != new_version {
changes.push(TicketChange::Version {
old: old_version,
new: new_version,
});
}
let old_ratio = self.done_ratio.unwrap_or(0);
let new_ratio = new.done_ratio.unwrap_or(0);
if old_ratio != new_ratio {
changes.push(TicketChange::DoneRatio {
old: format!("{}%", old_ratio),
new: format!("{}%", new_ratio),
});
}
changes
}
}
#[derive(Debug, Clone, Default)]
pub struct LabelColorMaps {
pub tracker_type: HashMap<String, (String, String)>,
pub priority: HashMap<String, (String, String)>,
}
#[async_trait]
pub trait TrackerProvider: Send + Sync {
fn name(&self) -> &'static str;
fn detect_ticket_id(&self, title: &str, description: &str) -> Option<String>;
async fn fetch_ticket(&self, ticket_id: &str) -> Option<LinkedTicket>;
fn ticket_url(&self, ticket_id: &str) -> String;
fn label_colors(&self) -> LabelColorMaps {
LabelColorMaps::default()
}
async fn fetch_activities(&self) -> Vec<Activity> {
vec![]
}
async fn fetch_time_entries(&self, _ticket_id: &str) -> Vec<TimeEntry> {
vec![]
}
async fn log_time(
&self,
_ticket_id: &str,
_entry: TimeEntryRequest,
) -> Result<(), TrackerError> {
Err(TrackerError::Unsupported(
"Time logging not supported by this tracker".into(),
))
}
}