Skip to content

g1t/apps/api/src/responses.rs

307 lines14,650 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1//! Every response the REST routes give, run through the converter the API
2//! sends them with, checked for `camelCase` that would leak out.
3//!
4//! The samples are the reference's example responses, put back into the
5//! `camelCase` the services send (as serde's `rename_all` writes it) and,
6//! where an operation returns a contract type, decoded into that type and
7//! encoded again, so that every field the type has is sent, not only the
8//! ones an example shows.
9
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge10use g1t_contracts::{access, actions, codeowners, integrations, repos, rules, search, teams, webhooks, work};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API11use g1t_kit::wire::{self, USER_KEYED};
12use serde::Serialize;
13use serde::de::DeserializeOwned;
14use serde_json::{Map, Value, json};
15
16use crate::openapi::document;
17use crate::operations::Op;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge18use crate::rules::RulesOp;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API19
20/// A key as `#[serde(rename_all = "camelCase")]` writes it.
21fn camel_key(key: &str) -> String {
22 let mut out = String::with_capacity(key.len());
23 let mut upper = false;
24 for c in key.chars() {
25 if c == '_' {
26 upper = true;
27 } else if upper {
28 out.extend(c.to_uppercase());
29 upper = false;
30 } else {
31 out.push(c);
32 }
33 }
34 out
35}
36
37/// A response as the services send it: `camelCase`, but for the maps the
38/// converter passes through, which are data.
39fn as_services_send(value: &Value) -> Value {
40 match value {
41 Value::Object(fields) => {
42 let mut out = Map::new();
43 for (key, value) in fields {
44 let user_keyed = value.is_object()
45 && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_"));
46 let value = if user_keyed { value.clone() } else { as_services_send(value) };
47 // A `by_…` map keeps its name in the converter's spelling.
48 let key = if key.starts_with("by_") { key.clone() } else { camel_key(key) };
49 out.insert(key, value);
50 }
51 Value::Object(out)
52 }
53 Value::Array(items) => Value::Array(items.iter().map(as_services_send).collect()),
54 other => other.clone(),
55 }
56}
57
58/// `value` decoded as `T` and encoded again, as the service would send it.
59fn through<T: DeserializeOwned + Serialize>(op: Op, value: Value) -> Value {
60 let decoded: T = serde_json::from_value(value)
61 .unwrap_or_else(|error| panic!("{}: the example is not a {}: {error}", op.name(), std::any::type_name::<T>()));
62 serde_json::to_value(decoded).unwrap()
63}
64
65/// What the service behind an operation sends, from its example.
66fn sample(op: Op, example: &Value) -> Value {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look67 // Types serde already writes in `snake_case`: a person, and who has
68 // access. Sent as they are.
69 let as_is = example.clone();
70 match op {
71 Op::Whoami => return through::<g1t_contracts::User>(op, as_is),
72 Op::ListCollaborators => return through::<access::RepoAccess>(op, as_is),
73 Op::AddCollaborator => return through::<access::Added>(op, as_is),
74 Op::UpdateCollaborator => return through::<access::Collaborator>(op, as_is),
75 Op::GetCollaboratorPermission => return through::<access::PermissionInfo>(op, as_is),
76 Op::ListRepoInvitations | Op::ListMyRepoInvitations => {
77 return through::<Vec<access::RepoInvitation>>(op, as_is);
78 }
79 Op::RevokeRepoInvitation | Op::AcceptRepoInvitation | Op::DeclineRepoInvitation => {
80 return through::<access::RepoInvitation>(op, as_is);
81 }
82 Op::ListOutsideCollaborators => return through::<Vec<access::OutsideCollaborator>>(op, as_is),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar83 // Teams and code owners, also `snake_case`.
84 Op::ListTeams | Op::ListChildTeams | Op::ListUserTeams => return through::<Vec<teams::Team>>(op, as_is),
85 Op::GetTeam | Op::CreateTeam | Op::UpdateTeam | Op::SetTeamReviewAssignment => {
86 return through::<teams::Team>(op, as_is);
87 }
88 Op::ListTeamMembers => return through::<Vec<teams::TeamMember>>(op, as_is),
89 Op::SetTeamMember => return through::<teams::TeamMember>(op, as_is),
90 Op::ListTeamRepos => return through::<Vec<teams::TeamRepo>>(op, as_is),
91 Op::SetTeamRepo => return through::<teams::TeamRepo>(op, as_is),
92 Op::DeleteTeam | Op::RemoveTeamMember | Op::RemoveTeamRepo => return through::<bool>(op, as_is),
93 Op::GetCodeownersErrors => return through::<codeowners::CodeOwnersReport>(op, as_is),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge94 // Rulesets travel in `snake_case` between services too.
95 Op::Rules(RulesOp::ListRepoRulesets | RulesOp::ListWorkspaceRulesets) => {
96 return through::<Vec<rules::Ruleset>>(op, as_is);
97 }
98 Op::Rules(
99 RulesOp::GetRepoRuleset
100 | RulesOp::CreateRepoRuleset
101 | RulesOp::UpdateRepoRuleset
102 | RulesOp::GetWorkspaceRuleset
103 | RulesOp::CreateWorkspaceRuleset
104 | RulesOp::UpdateWorkspaceRuleset,
105 ) => return through::<rules::Ruleset>(op, as_is),
106 Op::Rules(RulesOp::GetBranchRules) => return through::<rules::EffectiveRules>(op, as_is),
107 Op::Rules(RulesOp::ListRuleEvaluations | RulesOp::ListWorkspaceRuleEvaluations) => {
108 return through::<rules::EvaluationPage>(op, as_is);
109 }
110 // Built by the API itself.
111 Op::Rules(RulesOp::DeleteRepoRuleset | RulesOp::DeleteWorkspaceRuleset) => return as_is,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily112 // Built by the API itself, in `snake_case`.
113 Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is),
114 Op::DismissSecurityAlert | Op::ReopenSecurityAlert => {
115 return through::<crate::alerts::SecurityAlert>(op, as_is);
116 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look117 _ => {}
118 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar119 let mut sent = as_services_send(example);
120 // A pull request's code owners are `snake_case` inside it.
121 if op == Op::GetPullRequest
122 && let Some(code_owners) = example.get("code_owners")
123 {
124 sent["codeOwners"] = code_owners.clone();
125 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API126 match op {
Merge branch 'worktree-agent-ad7c6d88d93adc817'127 Op::GetWorkspace | Op::CreateWorkspace | Op::UpdateWorkspace => through::<g1t_contracts::identity::Workspace>(op, sent),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API128 Op::ListRepos => through::<Vec<repos::Repo>>(op, sent),
Search across all of g1t, Explore, and a command palette129 Op::Search => through::<search::SearchResults>(op, sent),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look130 Op::GetRepo
131 | Op::CreateRepo
132 | Op::UpdateRepo
133 | Op::TransferRepo
134 | Op::RenameRepo
135 | Op::RenameBranch
136 | Op::ArchiveRepo
137 | Op::UnarchiveRepo
138 | Op::SetRepoVisibility
139 | Op::RestoreRepo => through::<repos::Repo>(op, sent),
140 Op::DeleteRepo => through::<repos::DeletedRepo>(op, sent),
141 Op::ListDeletedRepos => through::<Vec<repos::DeletedRepo>>(op, sent),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API142 Op::GetRepoSettings | Op::UpdateRepoSettings => through::<work::RepoSettings>(op, sent),
Fast pages, required checks on the branch, self-hosted runners, honest incidents143 Op::ListCheckNames => through::<Vec<work::SeenCheck>>(op, sent),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API144 Op::GetMergeQueue => through::<work::QueueView>(op, sent),
145 Op::ListIssues => through::<Vec<work::Issue>>(op, sent),
146 Op::CreateIssue | Op::UpdateIssue | Op::CloseIssue | Op::ReopenIssue => {
147 through::<work::Issue>(op, sent)
148 }
149 Op::GetIssue => through::<work::IssueDetail>(op, sent),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step150 Op::Delegate => through::<work::Delegated>(op, sent),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API151 Op::ListPullRequests => through::<Vec<work::Pull>>(op, sent),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar152 Op::UpdatePullRequest => through::<work::Pull>(op, sent),
153 Op::ListLabels | Op::AddDefaultLabels | Op::ListIssueLabels => through::<Vec<work::Label>>(op, sent),
154 Op::CreateLabel | Op::UpdateLabel => through::<work::Label>(op, sent),
155 Op::ListMilestones => through::<Vec<work::Milestone>>(op, sent),
156 Op::CreateMilestone | Op::UpdateMilestone => through::<work::Milestone>(op, sent),
157 Op::GetMilestone => through::<work::MilestoneDetail>(op, sent),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API158 Op::GetPullRequest => through::<work::PullDetail>(op, sent),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar159 Op::MarkPullRequestReady
160 | Op::ClosePullRequest
161 | Op::MergePullRequest
162 | Op::AssignIssue
163 | Op::RequestReviewers
164 | Op::RemoveRequestedReviewers => {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API165 through::<work::Pull>(op, sent)
166 }
167 Op::ListWorkflows => through::<Vec<actions::Workflow>>(op, sent),
168 Op::ListWorkflowRuns => through::<Vec<actions::WorkflowRun>>(op, sent),
169 Op::GetWorkflowRun => through::<actions::RunDetail>(op, sent),
170 Op::GetJobLogs => through::<actions::JobLog>(op, sent),
171 Op::DispatchWorkflow | Op::CancelWorkflowRun | Op::RerunWorkflowRun => {
172 through::<actions::WorkflowRun>(op, sent)
173 }
174 Op::ListActionsSecrets | Op::ListActionsVariables => through::<Vec<actions::Setting>>(op, sent),
Fast pages, required checks on the branch, self-hosted runners, honest incidents175 Op::ListRunners => through::<Vec<g1t_contracts::runners::Runner>>(op, sent),
176 Op::ListRunnerGroups => through::<Vec<g1t_contracts::runners::RunnerGroup>>(op, sent),
177 Op::CreateRunnerGroup | Op::UpdateRunnerGroup => through::<g1t_contracts::runners::RunnerGroup>(op, sent),
178 Op::GetRunnerSettings | Op::UpdateRunnerSettings => through::<g1t_contracts::runners::RunnerSettings>(op, sent),
179 Op::CreateRunnerRegistrationToken => through::<g1t_contracts::runners::RegistrationToken>(op, sent),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API180 Op::ListWebhooks => through::<Vec<webhooks::Hook>>(op, sent),
181 Op::ListIntegrations => through::<Vec<integrations::Connection>>(op, sent),
182 Op::GetModelRoutes | Op::SetModelRoutes => through::<Vec<integrations::ModelRoute>>(op, sent),
183 Op::ListEvents => through::<Vec<g1t_contracts::events::Event>>(op, sent),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look184 Op::ListEmails | Op::AddEmail | Op::RemoveEmail | Op::UpdateEmailSettings => {
185 through::<g1t_contracts::accounts::AccountEmails>(op, sent)
186 }
187 Op::ListInvites => through::<g1t_contracts::identity::InvitesOverview>(op, sent),
188 Op::CreateInvite | Op::RevokeInvite | Op::InviteMember | Op::RevokeWorkspaceInvite => {
189 through::<g1t_contracts::identity::Invite>(op, sent)
190 }
191 Op::ListWorkspaceInvites => through::<Vec<g1t_contracts::identity::Invite>>(op, sent),
API: notifications over REST and MCP, with notifications scopes192 Op::ListNotifications => through::<g1t_contracts::inbox::InboxPage>(op, sent),
193 Op::GetNotificationThread | Op::MarkThreadRead | Op::MarkThreadDone | Op::SaveThread | Op::SnoozeThread => {
194 through::<g1t_contracts::inbox::InboxThread>(op, sent)
195 }
196 Op::GetThreadSubscription | Op::SetThreadSubscription | Op::DeleteThreadSubscription => {
197 through::<g1t_contracts::inbox::ThreadSubscription>(op, sent)
198 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API199 _ => sent,
200 }
201}
202
203/// Every key of `example`, as paths, outside the maps passed through.
204fn paths(value: &Value, path: &str, out: &mut Vec<String>) {
205 match value {
206 Value::Object(fields) => {
207 for (key, value) in fields {
208 let here = format!("{path}.{key}");
209 out.push(here.clone());
210 let user_keyed = value.is_object()
211 && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_"));
212 if !user_keyed {
213 paths(value, &here, out);
214 }
215 }
216 }
217 Value::Array(items) => {
218 for item in items {
219 paths(item, &format!("{path}[]"), out);
220 }
221 }
222 _ => {}
223 }
224}
225
226#[test]
227fn no_route_answers_with_camel_case() {
228 let document = document();
229 let (mut checked, mut converted) = (0, 0);
230 for (path, methods) in document["paths"].as_object().unwrap() {
231 for (method, operation) in methods.as_object().unwrap() {
232 let example = &operation["responses"]["200"]["content"]["application/json"]["example"];
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step233 let tool = operation["x-operation"].as_str().unwrap_or_default();
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API234 let Some(op) = Op::by_name(tool) else {
235 // Device sign-in, which is written in `snake_case` by hand.
236 assert!(wire::camel_case_keys(example).is_empty(), "{method} {path}");
237 continue;
238 };
239 let sample = sample(op, example);
240 converted += wire::camel_case_keys(&sample).len();
241 let sent = wire::snake_case(sample);
242 let leaked = wire::camel_case_keys(&sent);
243 assert!(leaked.is_empty(), "{method} {path} sends {leaked:?}");
244 // The reference shows what is sent: each of its names is one.
245 let (mut shown, mut real) = (Vec::new(), Vec::new());
246 paths(example, "", &mut shown);
247 paths(&sent, "", &mut real);
248 for name in shown {
249 assert!(real.contains(&name), "{method} {path}: the reference shows {name}, which is not sent");
250 }
251 checked += 1;
252 }
253 }
254 assert!(checked >= Op::ALL.len());
255 // The samples are in the services' spelling, so there was something to
256 // convert.
257 assert!(converted > 100, "{converted}");
258}
259
260#[test]
261fn every_route_has_a_sample() {
262 let document = document();
263 for route in crate::rest::ROUTES {
264 let path = route
265 .path
266 .split('/')
267 .map(|segment| match segment.strip_prefix(':') {
268 Some(name) => format!("{{{name}}}"),
269 None => segment.to_owned(),
270 })
271 .collect::<Vec<_>>()
272 .join("/");
273 let example = &document["paths"][&path][route.method.to_lowercase()]["responses"]["200"]
274 ["content"]["application/json"]["example"];
275 assert!(!example.is_null(), "{} {path}", route.method);
276 }
277}
278
279#[test]
280fn errors_and_reports_are_snake_case() {
281 let failure = g1t_contracts::Failure {
282 code: g1t_contracts::FailureCode::NotFound,
283 message: "No such endpoint.".to_owned(),
284 };
285 assert!(wire::camel_case_keys(&wire::snake_case(json!({ "error": failure }))).is_empty());
286}
287
288#[test]
289fn a_job_spec_keeps_github_s_spelling() {
290 let spec = json!({
291 "job": "job_1",
292 "spec": { "runs-on": "ubuntu-latest", "timeoutMinutes": 5 },
293 "workflow": { "env": { "nodeEnv": "x" } },
294 "github": { "eventName": "push", "headRef": "" },
295 "event": { "pull_request": { "headSha": "x" } },
296 "contexts": { "inputs": { "dryRun": true }, "matrix": { "nodeVersion": 20 } },
297 "checkout": { "ref": "main" },
298 "timeoutMinutes": 30,
299 "masks": [],
300 });
301 let sent = wire::snake_case_keeping(spec.clone(), crate::JOB_SPEC_AS_GIVEN);
302 assert_eq!(sent["timeout_minutes"], 30);
303 assert!(sent.get("timeoutMinutes").is_none());
304 for kept in ["spec", "workflow", "github", "event", "contexts", "checkout"] {
305 assert_eq!(sent[kept], spec[kept], "{kept}");
306 }
307}

This file's history is long; its oldest lines are credited to the oldest commit read.