Skip to main content

omni_dev/
provider.rs

1//! Provider-neutral issue/change-request types (#1779).
2//!
3//! A minimal, **GitHub-only** slice of the provider abstraction proposed in
4//! `#1573 §3.1`. [`GitProvider`] carries only the [`GitProvider::GitHub`]
5//! variant today; `#1573` adds `GitLab { host }` alongside it. Everything
6//! downstream of a fetch — question building, routing, and (later) decision
7//! verification — consumes only [`IssueDoc`] and friends, so a future GitLab
8//! fetcher slots in without touching that code.
9
10use serde::{Deserialize, Serialize};
11
12/// Which forge an [`IssueDoc`] (or [`ItemRef`]) came from.
13///
14/// Only [`GitHub`](Self::GitHub) exists today. `#1573` adds a `GitLab { host
15/// }` variant for self-hosted instances; nothing here should assume `GitHub`
16/// is the only case forever, but nothing needs to guard against a second
17/// variant yet either.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20pub enum GitProvider {
21    /// github.com. A GitHub Enterprise host is not supported yet: issue URLs
22    /// must be on github.com, and `gh` queries its default host.
23    GitHub,
24}
25
26/// Whether a referenced item is an issue or a change request (GitHub pull
27/// request / GitLab merge request).
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum ItemKind {
31    /// A tracking issue.
32    Issue,
33    /// A pull request (GitHub) or merge request (GitLab), named neutrally
34    /// per the issue's "use neutral wording from the start" guidance —
35    /// rewording later means re-validating every prompt that mentions it.
36    ChangeRequest,
37}
38
39/// Whether an item is still open.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "lowercase")]
42pub enum ItemState {
43    /// Open.
44    Open,
45    /// Closed (merged or not).
46    Closed,
47}
48
49/// A human-authored comment on an [`IssueDoc`].
50///
51/// Bot and system-event noise is filtered out by the fetch layer before an
52/// `IssueDoc` is built — this type only ever holds a human comment.
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub struct Comment {
55    /// The commenter's display/login name.
56    pub author: String,
57    /// The comment body (raw markdown, as authored).
58    pub body: String,
59    /// The comment's GitHub database id, used by `verify-decision`'s
60    /// `--comment ID` to select one. `None` in hand-built test fixtures.
61    #[serde(default)]
62    pub id: Option<u64>,
63}
64
65/// A reference to another issue or change request, e.g. one cited by a
66/// decision comment, or one that closed an issue.
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68pub struct ItemRef {
69    /// The provider this reference resolves against.
70    pub provider: GitProvider,
71    /// Opaque full project path (`"owner/repo"` on GitHub; GitLab paths
72    /// nest, per `#1573 §2.6`).
73    pub project: String,
74    /// Whether this reference is an issue or a change request.
75    pub kind: ItemKind,
76    /// The issue number / GitLab `iid`.
77    pub number: u64,
78}
79
80impl std::fmt::Display for ItemRef {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        write!(f, "{}#{}", self.project, self.number)
83    }
84}
85
86/// A fetched issue (or, in principle, change request) with everything a Jev
87/// question needs to judge it: title, body, human comments, and what closed
88/// it.
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90pub struct IssueDoc {
91    /// Which forge this came from.
92    pub provider: GitProvider,
93    /// Opaque full project path (`"owner/repo"` on GitHub).
94    pub project: String,
95    /// The issue number.
96    pub number: u64,
97    /// Always [`ItemKind::Issue`] today — `#1779` only ever routes issues,
98    /// never change requests, but the field stays generic for reuse by a
99    /// later `verify-decision` cited-source fetch.
100    pub kind: ItemKind,
101    /// The issue title.
102    pub title: String,
103    /// Whether the issue is open or closed.
104    pub state: ItemState,
105    /// The issue body (raw markdown, as authored).
106    pub body: String,
107    /// Human comments, in creation order. Bot and system-event comments are
108    /// already filtered out.
109    pub comments: Vec<Comment>,
110    /// Change requests that closed this issue, if any.
111    pub closed_by: Vec<ItemRef>,
112    /// The issue's web URL.
113    pub url: String,
114}
115
116#[cfg(test)]
117#[allow(clippy::unwrap_used, clippy::expect_used)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn item_ref_display_is_project_hash_number() {
123        let item_ref = ItemRef {
124            provider: GitProvider::GitHub,
125            project: "rust-works/omni-dev".to_string(),
126            kind: ItemKind::Issue,
127            number: 1779,
128        };
129        assert_eq!(item_ref.to_string(), "rust-works/omni-dev#1779");
130    }
131
132    #[test]
133    fn git_provider_serialises_lowercase() {
134        assert_eq!(
135            serde_json::to_value(GitProvider::GitHub).unwrap(),
136            serde_json::json!("github")
137        );
138    }
139
140    #[test]
141    fn item_kind_serialises_lowercase() {
142        assert_eq!(
143            serde_json::to_value(ItemKind::ChangeRequest).unwrap(),
144            serde_json::json!("changerequest")
145        );
146    }
147
148    #[test]
149    fn item_state_serialises_lowercase() {
150        assert_eq!(
151            serde_json::to_value(ItemState::Open).unwrap(),
152            serde_json::json!("open")
153        );
154        assert_eq!(
155            serde_json::to_value(ItemState::Closed).unwrap(),
156            serde_json::json!("closed")
157        );
158    }
159}