Skip to main content

forgetop_core/
domain.rs

1//! Provider-neutral domain model (mirrors the .NET `Forgetop.Core.Domain`).
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// The three top-level sections, each independently bound to a provider.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
8pub enum Section {
9    #[default]
10    PullRequests,
11    WorkItems,
12    Pipelines,
13}
14
15/// The platforms forgetop can talk to.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub enum ProviderType {
18    Demo,
19    GitHub,
20    AzureDevOps,
21    Linear,
22    GitLab,
23    Bitbucket,
24    Jira,
25}
26
27impl ProviderType {
28    pub fn as_str(&self) -> &'static str {
29        match self {
30            ProviderType::Demo => "Demo",
31            ProviderType::GitHub => "GitHub",
32            ProviderType::AzureDevOps => "AzureDevOps",
33            ProviderType::Linear => "Linear",
34            ProviderType::GitLab => "GitLab",
35            ProviderType::Bitbucket => "Bitbucket",
36            ProviderType::Jira => "Jira",
37        }
38    }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
42pub enum PullRequestStatus {
43    Open,
44    Draft,
45    Merged,
46    Closed,
47}
48
49/// Provider-neutral reviewer vote (ADO numeric votes / GitHub review states map here).
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51pub enum ReviewVote {
52    Rejected,
53    WaitingForAuthor,
54    NoVote,
55    ApprovedWithSuggestions,
56    Approved,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60pub enum WorkItemStateCategory {
61    Triage,
62    Backlog,
63    Unstarted,
64    Started,
65    Completed,
66    Canceled,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70pub enum PipelineRunStatus {
71    Queued,
72    Running,
73    Succeeded,
74    PartiallySucceeded,
75    Failed,
76    Canceled,
77}
78
79/// Roll-up CI/check state for a pull request.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
81pub enum CheckStatus {
82    #[default]
83    None,
84    Pending,
85    Passed,
86    Failed,
87}
88
89/// Whether a pull request can be merged.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
91pub enum MergeableState {
92    #[default]
93    Unknown,
94    Mergeable,
95    Blocked,
96    Conflicting,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
100pub enum FileChangeKind {
101    Added,
102    Modified,
103    Deleted,
104    Renamed,
105}
106
107// ---- entities ----
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct User {
111    pub id: String,
112    pub display_name: String,
113    pub handle: Option<String>,
114    pub avatar_url: Option<String>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct Repository {
119    pub id: String,
120    pub name: String,
121    pub full_name: Option<String>,
122    pub default_branch: Option<String>,
123    pub url: Option<String>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct Comment {
128    pub id: String,
129    pub author: User,
130    pub body: String,
131    pub created_at: Option<DateTime<Utc>>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct CommentThread {
136    pub id: String,
137    pub comments: Vec<Comment>,
138    pub file_path: Option<String>,
139    pub line: Option<i64>,
140    pub is_resolved: bool,
141}
142
143/// What happened in a timeline event — drives its icon/colour in the UI.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
145pub enum TimelineEventKind {
146    Approved,
147    ChangesRequested,
148    Reviewed,
149    Commented,
150    Merged,
151    Closed,
152    Reopened,
153    StateChanged,
154    Assigned,
155    Labeled,
156    Committed,
157    Other,
158}
159
160/// A single event on a pull request or work item (a review, a merge, a state change, an
161/// assignment, …), assembled from whatever the provider's timeline / activity / history API
162/// exposes. Ordered oldest → newest.
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct TimelineEvent {
165    pub actor: Option<User>,
166    pub kind: TimelineEventKind,
167    /// Human-readable one-liner, e.g. "approved", "changed status to In Progress", "merged".
168    pub summary: String,
169    pub at: Option<DateTime<Utc>>,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct Reviewer {
174    pub user: User,
175    pub vote: ReviewVote,
176    pub is_required: bool,
177}
178
179#[derive(Debug, Clone, Default, Serialize, Deserialize)]
180pub struct CheckSummary {
181    pub successful: u32,
182    pub in_progress: u32,
183    pub failed: u32,
184    pub neutral: u32,
185}
186
187impl CheckSummary {
188    pub fn total(&self) -> u32 {
189        self.successful + self.in_progress + self.failed + self.neutral
190    }
191}
192
193/// A single named CI check / status on a pull request.
194#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct CheckRun {
196    pub name: String,
197    pub status: CheckStatus,
198    pub url: Option<String>,
199}
200
201/// A commit on a pull request.
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct Commit {
204    pub sha: String,
205    pub message: String,
206    pub author: String,
207    pub date: Option<DateTime<Utc>>,
208    pub url: Option<String>,
209}
210
211/// Which side of a diff a line belongs to: the old (removed) or new (added) file.
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
213pub enum DiffSide {
214    Old,
215    New,
216}
217
218/// A pending review comment targeting a specific line of a file in a pull request.
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct LineComment {
221    pub path: String,
222    pub line: i64,
223    pub side: DiffSide,
224    pub body: String,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct FileChange {
229    pub path: String,
230    pub kind: FileChangeKind,
231    pub additions: i64,
232    pub deletions: i64,
233    /// Unified-diff patch text when the provider supplies it.
234    pub patch: Option<String>,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct PullRequest {
239    pub id: String,
240    /// The repository this pull request lives in, **connection-relative** (`acme/pay`) — see
241    /// [`crate::repo`]. `None` for providers that aren't repo-addressed (Jira, Linear).
242    #[serde(default, skip_serializing_if = "Option::is_none")]
243    pub repository: Option<String>,
244    pub number: Option<i64>,
245    pub title: String,
246    pub description: Option<String>,
247    pub author: User,
248    pub status: PullRequestStatus,
249    pub is_draft: bool,
250    pub source_ref: Option<String>,
251    pub target_ref: Option<String>,
252    pub reviewers: Vec<Reviewer>,
253    pub labels: Vec<String>,
254    pub checks: CheckStatus,
255    pub check_summary: Option<CheckSummary>,
256    pub mergeable: MergeableState,
257    pub changed_files: i64,
258    pub additions: i64,
259    pub deletions: i64,
260    pub created_at: Option<DateTime<Utc>>,
261    pub updated_at: Option<DateTime<Utc>>,
262    pub url: Option<String>,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct WorkItem {
267    pub id: String,
268    /// Where this item lives, **connection-relative** — a repository (`acme/pay`) for the
269    /// repo-addressed forges, or a bare project name for Azure DevOps, whose work items are
270    /// project-addressed rather than repo-addressed. `None` for Jira and Linear.
271    #[serde(default, skip_serializing_if = "Option::is_none")]
272    pub repository: Option<String>,
273    pub identifier: Option<String>,
274    pub title: String,
275    pub description: Option<String>,
276    pub state: String,
277    pub state_category: WorkItemStateCategory,
278    pub work_item_type: Option<String>,
279    pub assignee: Option<User>,
280    pub created_at: Option<DateTime<Utc>>,
281    pub updated_at: Option<DateTime<Utc>>,
282    pub url: Option<String>,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct PipelineDefinition {
287    pub id: String,
288    /// The repository this definition belongs to, **connection-relative**. `None` for providers
289    /// that aren't repo-addressed.
290    #[serde(default, skip_serializing_if = "Option::is_none")]
291    pub repository: Option<String>,
292    pub name: String,
293    pub path: Option<String>,
294    pub url: Option<String>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
298pub struct PipelineStep {
299    pub name: String,
300    pub status: PipelineRunStatus,
301    pub started_at: Option<DateTime<Utc>>,
302    pub finished_at: Option<DateTime<Utc>>,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct PipelineJob {
307    pub id: String,
308    pub name: String,
309    pub status: PipelineRunStatus,
310    pub started_at: Option<DateTime<Utc>>,
311    pub finished_at: Option<DateTime<Utc>>,
312    pub steps: Vec<PipelineStep>,
313    /// Deep link to this job in the provider's web UI.
314    pub url: Option<String>,
315    /// A short problem summary for failed jobs (provider-specific: GitLab's
316    /// failure reason, Azure's error/warning counts, etc.).
317    pub problem: Option<String>,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct PipelineStage {
322    pub name: String,
323    pub status: PipelineRunStatus,
324    pub jobs: Vec<PipelineJob>,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct PipelineRun {
329    pub id: String,
330    /// The repository this run belongs to, **connection-relative**. `None` for providers that
331    /// aren't repo-addressed.
332    #[serde(default, skip_serializing_if = "Option::is_none")]
333    pub repository: Option<String>,
334    pub definition_id: String,
335    pub number: Option<i64>,
336    pub name: Option<String>,
337    /// A human-readable title for the run — the triggering PR title or commit-message subject
338    /// where the provider exposes it (GitHub `display_title`). `None` when unavailable.
339    pub title: Option<String>,
340    pub status: PipelineRunStatus,
341    pub triggered_by: Option<User>,
342    pub branch: Option<String>,
343    pub commit_sha: Option<String>,
344    pub started_at: Option<DateTime<Utc>>,
345    pub finished_at: Option<DateTime<Utc>>,
346    pub url: Option<String>,
347    pub stages: Vec<PipelineStage>,
348}
349
350/// A gate on a pipeline run that is waiting for a manual decision (a deployment
351/// environment reviewer, an approval check, or a manual job).
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
353pub struct PipelineApproval {
354    /// Provider-native identifier used to act on this gate (environment id,
355    /// approval id, or manual job id, depending on the provider).
356    pub id: String,
357    /// Human label for the gate — usually the environment or stage name.
358    pub name: String,
359    /// Whether the authenticated user is allowed to respond to this gate.
360    pub can_respond: bool,
361}
362
363/// Addressing helpers: each item knows the repository it came from, so a call site never has to
364/// reconstruct one. Use these instead of building an [`crate::provider::ItemRef`] by hand.
365impl PullRequest {
366    pub fn item_ref(&self) -> crate::provider::ItemRef {
367        crate::provider::ItemRef::maybe(self.repository.clone(), self.id.clone())
368    }
369}
370
371impl WorkItem {
372    pub fn item_ref(&self) -> crate::provider::ItemRef {
373        crate::provider::ItemRef::maybe(self.repository.clone(), self.id.clone())
374    }
375}
376
377impl PipelineRun {
378    pub fn item_ref(&self) -> crate::provider::ItemRef {
379        crate::provider::ItemRef::maybe(self.repository.clone(), self.id.clone())
380    }
381
382    /// Addresses this run's *definition* (for a re-run/trigger), not the run itself.
383    pub fn definition_ref(&self) -> crate::provider::ItemRef {
384        crate::provider::ItemRef::maybe(self.repository.clone(), self.definition_id.clone())
385    }
386}
387
388impl PipelineDefinition {
389    pub fn item_ref(&self) -> crate::provider::ItemRef {
390        crate::provider::ItemRef::maybe(self.repository.clone(), self.id.clone())
391    }
392}
393
394/// A decision on a pending [`PipelineApproval`].
395#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
396pub enum ApprovalDecision {
397    Approve,
398    Reject,
399}
400
401/// Why a notification fired — each provider's native reason/action maps onto one of these.
402#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
403pub enum NotificationKind {
404    /// You were asked to review a PR/MR.
405    ReviewRequested,
406    /// You were @-mentioned (comment, description, team mention).
407    Mention,
408    /// Something was assigned to you.
409    Assigned,
410    /// CI failed on something of yours.
411    CiFailed,
412    /// A new comment/reply on something you follow.
413    Comment,
414    /// The state of something you follow changed (merged, closed, status).
415    StateChange,
416    /// Anything else the provider notified about.
417    Other,
418}
419
420/// What kind of item a notification points at — drives its icon and which source opens it.
421#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
422pub enum NotificationItemType {
423    PullRequest,
424    WorkItem,
425    Pipeline,
426    Other,
427}
428
429/// One item in the cross-provider notification inbox.
430#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
431pub struct Notification {
432    /// Provider-native notification id — used to mark it read.
433    pub id: String,
434    pub kind: NotificationKind,
435    pub item_type: NotificationItemType,
436    /// The underlying PR / work-item / pipeline id, for opening it in-app. `None` when the
437    /// notification has no resolvable item (then we fall back to the web URL).
438    pub item_id: Option<String>,
439    /// The repository the item lives in, **connection-relative** — needed to address `item_id`
440    /// on a connection that spans several repositories. `None` when the provider doesn't say.
441    #[serde(default, skip_serializing_if = "Option::is_none")]
442    pub repository: Option<String>,
443    /// Subject line — the PR/issue title.
444    pub title: String,
445    /// Where it lives: `org/repo`, or the project / team name.
446    pub context: String,
447    /// Web URL, for the browser fallback.
448    pub url: Option<String>,
449    pub unread: bool,
450    pub updated_at: Option<DateTime<Utc>>,
451}