g1t/services/work/src/rows.rs

334 lines10,907 bytesCodeBlame
1//! Rows as they come out of D1, and their conversion to contract types.
2
3use g1t_contracts::User;
4use g1t_contracts::repos::RepoPath;
5use g1t_contracts::work::{
6 CheckStatus, Comment, CommentKind, Issue, IssueReason, Pull, PullStatus, Runtime, SessionEntry,
7 SessionEntryKind, State, Verdict,
8};
9use serde::Deserialize;
10
11/// An author or actor as stored: g1t itself when the id is its own (a
12/// security update it opened, a merge its settings made), g1t's agent by
13/// its id (what it made or filed), a person otherwise.
14pub(crate) fn user(id: String, username: String) -> User {
15 let kind = if g1t_contracts::system::is_system_id(&id) {
16 g1t_contracts::PrincipalKind::System
17 } else if id == g1t_contracts::identity::AGENT_ID {
18 g1t_contracts::PrincipalKind::Agent
19 } else {
20 g1t_contracts::PrincipalKind::User
21 };
22 User {
23 id,
24 username,
25 kind,
26 ..User::default()
27 }
28}
29
30/// Who asked g1t for the work, as stored beside its author: both columns,
31/// or nobody.
32pub(crate) fn requester(id: Option<String>, username: Option<String>) -> Option<User> {
33 Some(user(id?, username?))
34}
35
36#[derive(Deserialize)]
37pub struct IssueRow {
38 pub id: String,
39 pub repo_id: String,
40 pub number: u32,
41 pub title: String,
42 pub body: String,
43 /// JSON array of label names.
44 pub labels: String,
45 pub state: State,
46 pub reason: Option<IssueReason>,
47 pub resolved_by: Option<u32>,
48 pub author_id: String,
49 pub author_name: String,
50 #[serde(default)]
51 pub requested_by_id: Option<String>,
52 #[serde(default)]
53 pub requested_by_name: Option<String>,
54 pub created_at: String,
55 pub updated_at: String,
56 pub closed_at: Option<String>,
57 pub pull_count: u32,
58 pub comment_count: u32,
59 /// JSON array of usernames.
60 pub assignees: String,
61 /// JSON array of issue numbers.
62 pub blocked_by: String,
63 /// JSON of who queued it for a g1t agent, if anyone has.
64 pub queued_by: Option<String>,
65 pub agent: Option<String>,
66}
67
68impl From<IssueRow> for Issue {
69 fn from(row: IssueRow) -> Self {
70 Issue {
71 id: row.id,
72 repo_id: row.repo_id,
73 number: row.number,
74 title: row.title,
75 body: row.body,
76 labels: serde_json::from_str(&row.labels).unwrap_or_default(),
77 state: row.state,
78 reason: row.reason,
79 resolved_by: row.resolved_by,
80 author: user(row.author_id, row.author_name),
81 requested_by: requester(row.requested_by_id, row.requested_by_name),
82 created_at: row.created_at,
83 updated_at: row.updated_at,
84 closed_at: row.closed_at,
85 pull_count: row.pull_count,
86 comment_count: row.comment_count,
87 assignees: serde_json::from_str(&row.assignees).unwrap_or_default(),
88 blocked_by: serde_json::from_str(&row.blocked_by).unwrap_or_default(),
89 queued: row.queued_by.is_some(),
90 agent: row.agent,
91 }
92 }
93}
94
95/// The remembered assessment of a pull request, read beside its row.
96#[derive(Deserialize)]
97pub struct Snapshot {
98 pub stage: Option<g1t_contracts::work::Stage>,
99 pub stage_detail: Option<String>,
100 #[serde(default)]
101 pub revisions: u32,
102 /// Whether g1t is seeing it through at all.
103 #[serde(default)]
104 pub managed: u32,
105}
106
107/// A pull request's columns, with its confidence (confidence.rs) beside
108/// them: what every read of a [`PullRow`] selects.
109pub const PULL_COLUMNS: &str =
110 "pulls.*, (SELECT detail FROM pull_confidence WHERE pull_confidence.pull_id = pulls.id) AS confidence";
111
112#[derive(Deserialize)]
113pub struct PullRow {
114 pub id: String,
115 pub repo_id: String,
116 pub number: u32,
117 pub issue_number: Option<u32>,
118 pub title: String,
119 pub body: Option<String>,
120 pub agent: String,
121 pub runtime: Runtime,
122 pub status: PullStatus,
123 pub fork_repo_id: Option<String>,
124 pub fork_namespace: Option<String>,
125 pub fork_name: Option<String>,
126 pub source_branch: Option<String>,
127 pub head_commit: Option<String>,
128 pub merge_base: Option<String>,
129 pub merged_by: Option<String>,
130 pub merged_at: Option<String>,
131 pub superseded_by: Option<u32>,
132 pub check_status: Option<CheckStatus>,
133 /// JSON array of changed files; `None` until first worked out.
134 pub files: Option<String>,
135 /// JSON arrays of usernames.
136 pub assignees: String,
137 pub reviewers: String,
138 pub author_id: String,
139 pub author_name: String,
140 #[serde(default)]
141 pub requested_by_id: Option<String>,
142 #[serde(default)]
143 pub requested_by_name: Option<String>,
144 pub created_at: String,
145 pub updated_at: String,
146 /// JSON of its confidence, once worked out.
147 #[serde(default)]
148 pub confidence: Option<String>,
149}
150
151impl From<PullRow> for Pull {
152 fn from(row: PullRow) -> Self {
153 Pull {
154 id: row.id,
155 repo_id: row.repo_id,
156 number: row.number,
157 issue: row.issue_number,
158 title: row.title,
159 body: row.body,
160 agent: row.agent,
161 runtime: row.runtime,
162 status: row.status,
163 fork: row
164 .fork_namespace
165 .zip(row.fork_name)
166 .map(|(namespace, name)| RepoPath { namespace, name }),
167 fork_repo_id: row.fork_repo_id,
168 branch: row.source_branch,
169 head_commit: row.head_commit,
170 merge_base: row.merge_base,
171 merged_by: row.merged_by,
172 merged_at: row.merged_at,
173 superseded_by: row.superseded_by,
174 check_status: row.check_status,
175 files: row
176 .files
177 .as_deref()
178 .and_then(|files| serde_json::from_str(files).ok())
179 .unwrap_or_default(),
180 assignees: serde_json::from_str(&row.assignees).unwrap_or_default(),
181 reviewers: serde_json::from_str(&row.reviewers).unwrap_or_default(),
182 author: user(row.author_id, row.author_name),
183 requested_by: requester(row.requested_by_id, row.requested_by_name),
184 created_at: row.created_at,
185 updated_at: row.updated_at,
186 confidence: row
187 .confidence
188 .as_deref()
189 .and_then(|detail| serde_json::from_str(detail).ok()),
190 }
191 }
192}
193
194#[derive(Deserialize)]
195pub struct CommentRow {
196 pub id: String,
197 pub kind: CommentKind,
198 pub author_id: String,
199 pub author_name: String,
200 pub body: String,
201 pub path: Option<String>,
202 pub line: Option<u32>,
203 pub verdict: Option<Verdict>,
204 pub created_at: String,
205}
206
207impl From<CommentRow> for Comment {
208 fn from(row: CommentRow) -> Self {
209 Comment {
210 id: row.id,
211 kind: row.kind,
212 author: user(row.author_id, row.author_name),
213 body: row.body,
214 path: row.path,
215 line: row.line,
216 verdict: row.verdict,
217 created_at: row.created_at,
218 }
219 }
220}
221
222#[derive(Deserialize)]
223pub struct SessionRow {
224 pub seq: u32,
225 pub kind: SessionEntryKind,
226 pub text: String,
227 pub tool: Option<String>,
228 pub commit: Option<String>,
229 pub at: String,
230}
231
232impl From<SessionRow> for SessionEntry {
233 fn from(row: SessionRow) -> Self {
234 SessionEntry {
235 seq: row.seq,
236 kind: row.kind,
237 text: row.text,
238 tool: row.tool,
239 commit: row.commit,
240 at: row.at,
241 }
242 }
243}
244
245/// A pull request whose head a push moved.
246#[derive(Deserialize)]
247pub struct MovedRow {
248 pub id: String,
249 pub repo_id: String,
250 pub number: u32,
251 pub issue_number: Option<u32>,
252 pub status: PullStatus,
253 pub author_id: String,
254 pub author_name: String,
255 #[serde(default)]
256 pub requested_by_id: Option<String>,
257 #[serde(default)]
258 pub requested_by_name: Option<String>,
259}
260
261/// A single number selected as `n`.
262#[derive(Deserialize)]
263pub struct NumberRow {
264 pub n: u32,
265}
266
267/// A single string selected as `value`.
268#[derive(Deserialize)]
269pub struct ValueRow {
270 pub value: String,
271}
272
273/// Rows as the database holds them, for tests of the rules that read them.
274#[cfg(test)]
275pub(crate) mod stored {
276 use super::*;
277
278 /// A pull request g1t made in a fork, read back as stored: g1t as its
279 /// author, and `requested_by` the person who asked, if anyone did.
280 pub(crate) fn pull(author: (&str, &str), requested_by: Option<(&str, &str)>) -> Pull {
281 let row: PullRow = serde_json::from_value(serde_json::json!({
282 "id": "pr_1", "repo_id": "rep_1", "number": 14, "issue_number": 12, "title": "Fix it",
283 "body": null, "agent": "g1t", "runtime": "hosted", "status": "open",
284 "fork_repo_id": "rep_f", "fork_namespace": "pulls", "fork_name": "pr_1",
285 "source_branch": null, "head_commit": null, "merge_base": null, "merged_by": null,
286 "merged_at": null, "superseded_by": null, "check_status": null, "files": null,
287 "assignees": "[]", "reviewers": "[]",
288 "author_id": author.0, "author_name": author.1,
289 "requested_by_id": requested_by.map(|(id, _)| id),
290 "requested_by_name": requested_by.map(|(_, name)| name),
291 "created_at": "", "updated_at": ""
292 }))
293 .unwrap();
294 row.into()
295 }
296
297 /// g1t's agent, as an author is stored.
298 pub(crate) const G1T: (&str, &str) = (g1t_contracts::identity::AGENT_ID, "g1t");
299 /// The person who asked g1t for the work.
300 pub(crate) const ASKER: (&str, &str) = ("usr_1", "syntaqx");
301}
302
303#[cfg(test)]
304mod tests {
305 use super::stored::{ASKER, G1T, pull};
306 use super::*;
307 use g1t_contracts::PrincipalKind;
308
309 #[test]
310 fn g1t_s_pull_request_reads_back_as_g1t_s_requested_by_the_person() {
311 let made = pull(G1T, Some(ASKER));
312 assert_eq!((made.author.username.as_str(), made.author.kind), ("g1t", PrincipalKind::Agent));
313 let asked = made.requested_by.as_ref().expect("who asked is kept");
314 assert_eq!((asked.id.as_str(), asked.username.as_str(), asked.kind), ("usr_1", "syntaqx", PrincipalKind::User));
315 assert_eq!(made.owner().id, "usr_1");
316 }
317
318 #[test]
319 fn anyone_else_s_reads_back_as_theirs_alone() {
320 let own = pull(ASKER, None);
321 assert_eq!(own.author.kind, PrincipalKind::User);
322 assert!(own.requested_by.is_none());
323 assert_eq!(own.owner().id, "usr_1");
324 // Half a requester is nobody.
325 assert!(requester(Some("usr_1".into()), None).is_none());
326 }
327
328 #[test]
329 fn g1t_s_own_work_reads_back_as_g1t_itself() {
330 let own = pull(("g1t", "g1t"), None);
331 assert_eq!(own.author.kind, PrincipalKind::System);
332 assert!(own.requested_by.is_none());
333 }
334}