gitlab_tracker_core/provider.rs
1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3
4/// A lightweight reference to an external tracker ticket linked to a MR.
5///
6/// This struct is the only data type exchanged between the orchestrator
7/// (`gitlab-tracker`) and any tracker plugin (Redmine, Jira, Trello, …).
8/// It is intentionally flat and display-oriented — the orchestrator does not
9/// need to know anything about the internal data model of the tracker.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct LinkedTicket {
12 /// Numeric or alphanumeric identifier as shown in the tracker UI (e.g. "1234", "PROJ-42").
13 pub id: String,
14 /// Short title / subject of the ticket.
15 pub subject: String,
16 /// Human-readable status label (e.g. "In Progress", "Resolved").
17 pub status: String,
18 /// Direct URL to open the ticket in a browser.
19 pub url: String,
20 /// Original creator of the ticket (display name).
21 /// `None` when not provided by the tracker.
22 #[serde(default)]
23 pub author: Option<String>,
24 /// User currently assigned to the ticket (display name).
25 /// `None` when unassigned or not supported by the tracker.
26 #[serde(default)]
27 pub assignee: Option<String>,
28 /// Estimated time to complete the ticket, in seconds (e.g. from Redmine `/estimate`).
29 /// `None` when not set or not supported by the tracker.
30 #[serde(default)]
31 pub time_estimate: Option<u32>,
32 /// Time already spent on the ticket, in seconds (e.g. from Redmine time entries).
33 /// `None` when not set or not supported by the tracker.
34 #[serde(default)]
35 pub time_spent: Option<u32>,
36}
37
38/// A time-tracking activity category as defined in the tracker (e.g. Redmine enumerations).
39///
40/// Activities are fetched once at startup and cached in `App` to populate
41/// the Log Time popup selector without a network round-trip per keypress.
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43pub struct Activity {
44 /// Numeric identifier used when submitting a time entry via the API.
45 pub id: u32,
46 /// Human-readable label shown in the popup selector (e.g. "Development", "Design").
47 pub name: String,
48}
49
50/// A single time entry recorded on a tracker ticket.
51///
52/// Returned by `fetch_time_entries` and displayed in the Inspector's TimeLog view.
53#[derive(Debug, Clone)]
54pub struct TimeEntry {
55 /// Internal tracker identifier for this entry.
56 pub id: u64,
57 /// Duration in hours as stored by the tracker.
58 pub hours: f32,
59 /// Activity category associated with this entry.
60 pub activity: Activity,
61 /// Free-text comment left by the user when logging time.
62 pub comment: String,
63 /// Display name of the user who logged the time.
64 pub user: String,
65 /// Date on which the time was spent, in `YYYY-MM-DD` format.
66 pub spent_on: String,
67}
68
69/// Payload sent to `log_time` when the user confirms the Log Time popup.
70///
71/// All fields are required — the popup enforces them before submission.
72#[derive(Debug, Clone)]
73pub struct TimeEntryRequest {
74 /// Duration in hours (e.g. `1.5` for 1h30).
75 pub hours: f32,
76 /// Identifier of the selected activity category.
77 pub activity_id: u32,
78 /// Optional free-text comment. May be empty.
79 pub comment: String,
80 /// Date on which the time was spent, in `YYYY-MM-DD` format (defaults to today).
81 pub spent_on: String,
82}
83
84/// Contract that every external tracker integration must implement.
85///
86/// # Design
87/// - The orchestrator holds a `Box<dyn TrackerProvider + Send + Sync>` and
88/// calls `detect_ticket_id` + `fetch_ticket` without knowing the concrete type.
89/// - Each implementation lives in its own crate (e.g. `gitlab-tracker-redmine`).
90/// - Adding a new tracker (Jira, Trello, Linear …) only requires a new crate
91/// that implements this trait — no change to the orchestrator logic.
92///
93/// # Thread safety
94/// `Send + Sync` is required because the provider is shared across async tasks.
95#[async_trait]
96pub trait TrackerProvider: Send + Sync {
97 /// Human-readable name of the tracker (e.g. "Redmine", "Jira").
98 /// Used for logging and UI labels.
99 fn name(&self) -> &'static str;
100
101 /// Attempts to extract a ticket identifier from the MR title and/or description.
102 ///
103 /// Returns the raw ticket id string (e.g. "1234") when found, `None` otherwise.
104 /// Implementations should prefer the title over the description when both match.
105 fn detect_ticket_id(&self, title: &str, description: &str) -> Option<String>;
106
107 /// Fetches the full ticket details for the given raw ticket id.
108 ///
109 /// Returns `None` on network error, authentication failure, or when the
110 /// ticket does not exist. The caller is responsible for caching results.
111 async fn fetch_ticket(&self, ticket_id: &str) -> Option<LinkedTicket>;
112
113 /// Builds the direct URL to the ticket from its id.
114 ///
115 /// This is a pure helper that may be called without a network round-trip
116 /// (e.g. to open the browser immediately on keypress while the fetch is pending).
117 fn ticket_url(&self, ticket_id: &str) -> String;
118
119 /// Fetches the list of time-tracking activity categories available in the tracker.
120 ///
121 /// Called once at startup (or on first popup open) and cached in `App`.
122 /// Default implementation returns an empty list (opt-in capability).
123 async fn fetch_activities(&self) -> Vec<Activity> {
124 vec![]
125 }
126
127 /// Fetches all time entries recorded on the given ticket.
128 ///
129 /// Displayed in the Inspector's TimeLog view. Default returns empty (opt-in).
130 async fn fetch_time_entries(&self, _ticket_id: &str) -> Vec<TimeEntry> {
131 vec![]
132 }
133
134 /// Submits a new time entry on the given ticket.
135 ///
136 /// Returns `Ok(())` on success, or an error message string suitable for
137 /// displaying inline in the TUI. Default returns an unsupported error.
138 async fn log_time(&self, _ticket_id: &str, _entry: TimeEntryRequest) -> Result<(), String> {
139 Err("Time logging not supported by this tracker".into())
140 }
141}