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}