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.
| sudo: discounts listed among what was given away | 1 | //! 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 checks: statuses and check runs on every commit | 10 | use g1t_contracts::{access, actions, checks, codeowners, integrations, repos, rules, search, teams, webhooks, work}; |
| sudo: discounts listed among what was given away | 11 | use g1t_kit::wire::{self, USER_KEYED}; |
| 12 | use serde::Serialize; | |
| 13 | use serde::de::DeserializeOwned; | |
| 14 | use serde_json::{Map, Value, json}; | |
| 15 | ||
| 16 | use crate::openapi::document; | |
| 17 | use crate::operations::Op; | |
| Merge checks: statuses and check runs on every commit | 18 | use crate::checks::ChecksOp; |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 19 | use crate::rules::RulesOp; |
| Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca | 20 | use crate::deploy_keys::DeployKeysOp; |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 21 | use crate::coordination::CoordinationOp; |
| sudo: discounts listed among what was given away | 22 | |
| 23 | /// A key as `#[serde(rename_all = "camelCase")]` writes it. | |
| 24 | fn camel_key(key: &str) -> String { | |
| 25 | let mut out = String::with_capacity(key.len()); | |
| 26 | let mut upper = false; | |
| 27 | for c in key.chars() { | |
| 28 | if c == '_' { | |
| 29 | upper = true; | |
| 30 | } else if upper { | |
| 31 | out.extend(c.to_uppercase()); | |
| 32 | upper = false; | |
| 33 | } else { | |
| 34 | out.push(c); | |
| 35 | } | |
| 36 | } | |
| 37 | out | |
| 38 | } | |
| 39 | ||
| 40 | /// A response as the services send it: `camelCase`, but for the maps the | |
| 41 | /// converter passes through, which are data. | |
| 42 | fn as_services_send(value: &Value) -> Value { | |
| 43 | match value { | |
| 44 | Value::Object(fields) => { | |
| 45 | let mut out = Map::new(); | |
| 46 | for (key, value) in fields { | |
| 47 | let user_keyed = value.is_object() | |
| 48 | && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_")); | |
| 49 | let value = if user_keyed { value.clone() } else { as_services_send(value) }; | |
| 50 | // A `by_…` map keeps its name in the converter's spelling. | |
| 51 | let key = if key.starts_with("by_") { key.clone() } else { camel_key(key) }; | |
| 52 | out.insert(key, value); | |
| 53 | } | |
| 54 | Value::Object(out) | |
| 55 | } | |
| 56 | Value::Array(items) => Value::Array(items.iter().map(as_services_send).collect()), | |
| 57 | other => other.clone(), | |
| 58 | } | |
| 59 | } | |
| 60 | ||
| 61 | /// `value` decoded as `T` and encoded again, as the service would send it. | |
| 62 | fn through<T: DeserializeOwned + Serialize>(op: Op, value: Value) -> Value { | |
| 63 | let decoded: T = serde_json::from_value(value) | |
| 64 | .unwrap_or_else(|error| panic!("{}: the example is not a {}: {error}", op.name(), std::any::type_name::<T>())); | |
| 65 | serde_json::to_value(decoded).unwrap() | |
| 66 | } | |
| 67 | ||
| 68 | /// What the service behind an operation sends, from its example. | |
| 69 | fn sample(op: Op, example: &Value) -> Value { | |
| 70 | // Types serde already writes in `snake_case`: a person, and who has | |
| 71 | // access. Sent as they are. | |
| 72 | let as_is = example.clone(); | |
| 73 | match op { | |
| 74 | Op::Whoami => return through::<g1t_contracts::User>(op, as_is), | |
| 75 | Op::ListCollaborators => return through::<access::RepoAccess>(op, as_is), | |
| 76 | Op::AddCollaborator => return through::<access::Added>(op, as_is), | |
| 77 | Op::UpdateCollaborator => return through::<access::Collaborator>(op, as_is), | |
| 78 | Op::GetCollaboratorPermission => return through::<access::PermissionInfo>(op, as_is), | |
| 79 | Op::ListRepoInvitations | Op::ListMyRepoInvitations => { | |
| 80 | return through::<Vec<access::RepoInvitation>>(op, as_is); | |
| 81 | } | |
| 82 | Op::RevokeRepoInvitation | Op::AcceptRepoInvitation | Op::DeclineRepoInvitation => { | |
| 83 | return through::<access::RepoInvitation>(op, as_is); | |
| 84 | } | |
| 85 | Op::ListOutsideCollaborators => return through::<Vec<access::OutsideCollaborator>>(op, as_is), | |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 86 | // Members, also `snake_case`. |
| 87 | Op::ListMembers => return through::<Vec<g1t_contracts::identity::Member>>(op, as_is), | |
| 88 | Op::UpdateMember => return through::<g1t_contracts::identity::Member>(op, as_is), | |
| 89 | Op::RemoveMember | Op::TransferOwnership | Op::LeaveWorkspace => return through::<bool>(op, as_is), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 90 | // Teams and code owners, also `snake_case`. |
| 91 | Op::ListTeams | Op::ListChildTeams | Op::ListUserTeams => return through::<Vec<teams::Team>>(op, as_is), | |
| 92 | Op::GetTeam | Op::CreateTeam | Op::UpdateTeam | Op::SetTeamReviewAssignment => { | |
| 93 | return through::<teams::Team>(op, as_is); | |
| 94 | } | |
| 95 | Op::ListTeamMembers => return through::<Vec<teams::TeamMember>>(op, as_is), | |
| 96 | Op::SetTeamMember => return through::<teams::TeamMember>(op, as_is), | |
| 97 | Op::ListTeamRepos => return through::<Vec<teams::TeamRepo>>(op, as_is), | |
| 98 | Op::SetTeamRepo => return through::<teams::TeamRepo>(op, as_is), | |
| 99 | Op::DeleteTeam | Op::RemoveTeamMember | Op::RemoveTeamRepo => return through::<bool>(op, as_is), | |
| 100 | Op::GetCodeownersErrors => return through::<codeowners::CodeOwnersReport>(op, as_is), | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 101 | // Rulesets travel in `snake_case` between services too. |
| 102 | Op::Rules(RulesOp::ListRepoRulesets | RulesOp::ListWorkspaceRulesets) => { | |
| 103 | return through::<Vec<rules::Ruleset>>(op, as_is); | |
| 104 | } | |
| 105 | Op::Rules( | |
| 106 | RulesOp::GetRepoRuleset | |
| 107 | | RulesOp::CreateRepoRuleset | |
| 108 | | RulesOp::UpdateRepoRuleset | |
| 109 | | RulesOp::GetWorkspaceRuleset | |
| 110 | | RulesOp::CreateWorkspaceRuleset | |
| 111 | | RulesOp::UpdateWorkspaceRuleset, | |
| 112 | ) => return through::<rules::Ruleset>(op, as_is), | |
| 113 | Op::Rules(RulesOp::GetBranchRules) => return through::<rules::EffectiveRules>(op, as_is), | |
| 114 | Op::Rules(RulesOp::ListRuleEvaluations | RulesOp::ListWorkspaceRuleEvaluations) => { | |
| 115 | return through::<rules::EvaluationPage>(op, as_is); | |
| 116 | } | |
| 117 | // Built by the API itself. | |
| 118 | Op::Rules(RulesOp::DeleteRepoRuleset | RulesOp::DeleteWorkspaceRuleset) => return as_is, | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 119 | // Deployments travel in `snake_case` between services too. |
| 120 | Op::Deployments(_) => return as_is, | |
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 121 | // So does telemetry. |
| 122 | Op::Telemetry(_) => return as_is, | |
| Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2 | 123 | // Artifacts are shaped by the API itself, in `snake_case`. |
| 124 | Op::Artifacts(_) => return as_is, | |
| Merge branch 'worktree-agent-a3b9b1f730f55b5bf' | 125 | // The cache's entries travel in `snake_case` from the actions service. |
| 126 | Op::Caches(_) => return as_is, | |
| Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's. | 127 | // So are archive links. |
| 128 | Op::Archives(_) => return as_is, | |
| Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca | 129 | // Deploy keys travel in `snake_case` from identity. |
| 130 | Op::DeployKeys(DeployKeysOp::ListDeployKeys) => return through::<Vec<g1t_contracts::deploy_keys::DeployKey>>(op, as_is), | |
| 131 | Op::DeployKeys(DeployKeysOp::GetDeployKey | DeployKeysOp::CreateDeployKey) => { | |
| 132 | return through::<g1t_contracts::deploy_keys::DeployKey>(op, as_is); | |
| 133 | } | |
| 134 | Op::DeployKeys(DeployKeysOp::DeleteDeployKey) => return through::<bool>(op, as_is), | |
| Merge packages: roles, Actions access, source label, soft delete, API | 135 | // Packages are shaped by the API itself, in `snake_case`. |
| 136 | Op::Packages(_) => return as_is, | |
| Merge main into Artifacts Phase 2 | 137 | // Artifacts are shaped by the API itself, in `snake_case`. |
| 138 | Op::Folios(_) => return as_is, | |
| sudo: discounts listed among what was given away | 139 | // Built by the API itself, in `snake_case`. |
| 140 | Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is), | |
| 141 | Op::DismissSecurityAlert | Op::ReopenSecurityAlert => { | |
| 142 | return through::<crate::alerts::SecurityAlert>(op, as_is); | |
| 143 | } | |
| 144 | _ => {} | |
| 145 | } | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 146 | let mut sent = as_services_send(example); |
| 147 | // A pull request's code owners are `snake_case` inside it. | |
| 148 | if op == Op::GetPullRequest | |
| 149 | && let Some(code_owners) = example.get("code_owners") | |
| 150 | { | |
| 151 | sent["codeOwners"] = code_owners.clone(); | |
| 152 | } | |
| sudo: discounts listed among what was given away | 153 | match op { |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 154 | Op::GetWorkspace | Op::CreateWorkspace | Op::UpdateWorkspace => through::<g1t_contracts::identity::Workspace>(op, sent), |
| sudo: discounts listed among what was given away | 155 | Op::ListRepos => through::<Vec<repos::Repo>>(op, sent), |
| 156 | Op::Search => through::<search::SearchResults>(op, sent), | |
| 157 | Op::GetRepo | |
| 158 | | Op::CreateRepo | |
| 159 | | Op::UpdateRepo | |
| 160 | | Op::TransferRepo | |
| 161 | | Op::RenameRepo | |
| 162 | | Op::RenameBranch | |
| 163 | | Op::ArchiveRepo | |
| 164 | | Op::UnarchiveRepo | |
| 165 | | Op::SetRepoVisibility | |
| 166 | | Op::RestoreRepo => through::<repos::Repo>(op, sent), | |
| The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days. | 167 | Op::CreateBranch | Op::DeleteBranch => through::<repos::Branch>(op, sent), |
| sudo: discounts listed among what was given away | 168 | Op::DeleteRepo => through::<repos::DeletedRepo>(op, sent), |
| 169 | Op::ListDeletedRepos => through::<Vec<repos::DeletedRepo>>(op, sent), | |
| 170 | Op::GetRepoSettings | Op::UpdateRepoSettings => through::<work::RepoSettings>(op, sent), | |
| 171 | Op::ListCheckNames => through::<Vec<work::SeenCheck>>(op, sent), | |
| 172 | Op::GetMergeQueue => through::<work::QueueView>(op, sent), | |
| Merge branch 'worktree-agent-ab8033ab973f8d99a' | 173 | Op::ListDecisions => through::<Vec<g1t_contracts::decisions::Decision>>(op, sent), |
| 174 | Op::RecordDecision => through::<g1t_contracts::decisions::Decision>(op, sent), | |
| 175 | Op::Why => through::<g1t_contracts::decisions::Why>(op, sent), | |
| sudo: discounts listed among what was given away | 176 | Op::ListIssues => through::<Vec<work::Issue>>(op, sent), |
| 177 | Op::CreateIssue | Op::UpdateIssue | Op::CloseIssue | Op::ReopenIssue => { | |
| 178 | through::<work::Issue>(op, sent) | |
| 179 | } | |
| 180 | Op::GetIssue => through::<work::IssueDetail>(op, sent), | |
| 181 | Op::ListPullRequests => through::<Vec<work::Pull>>(op, sent), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 182 | Op::UpdatePullRequest => through::<work::Pull>(op, sent), |
| 183 | Op::ListLabels | Op::AddDefaultLabels | Op::ListIssueLabels => through::<Vec<work::Label>>(op, sent), | |
| 184 | Op::CreateLabel | Op::UpdateLabel => through::<work::Label>(op, sent), | |
| 185 | Op::ListMilestones => through::<Vec<work::Milestone>>(op, sent), | |
| 186 | Op::CreateMilestone | Op::UpdateMilestone => through::<work::Milestone>(op, sent), | |
| 187 | Op::GetMilestone => through::<work::MilestoneDetail>(op, sent), | |
| sudo: discounts listed among what was given away | 188 | Op::GetPullRequest => through::<work::PullDetail>(op, sent), |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 189 | Op::MarkPullRequestReady |
| 190 | | Op::ClosePullRequest | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 191 | | Op::ReopenPullRequest |
| 192 | | Op::ConvertPullRequestToDraft | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 193 | | Op::MergePullRequest |
| 194 | | Op::RequestReviewers | |
| 195 | | Op::RemoveRequestedReviewers => { | |
| sudo: discounts listed among what was given away | 196 | through::<work::Pull>(op, sent) |
| 197 | } | |
| 198 | Op::ListWorkflows => through::<Vec<actions::Workflow>>(op, sent), | |
| Merge branch 'worktree-agent-a4db4b523bb973765' | 199 | Op::GetWorkflow => through::<actions::WorkflowDetail>(op, sent), |
| sudo: discounts listed among what was given away | 200 | Op::ListWorkflowRuns => through::<Vec<actions::WorkflowRun>>(op, sent), |
| 201 | Op::GetWorkflowRun => through::<actions::RunDetail>(op, sent), | |
| 202 | Op::GetJobLogs => through::<actions::JobLog>(op, sent), | |
| 203 | Op::DispatchWorkflow | Op::CancelWorkflowRun | Op::RerunWorkflowRun => { | |
| 204 | through::<actions::WorkflowRun>(op, sent) | |
| 205 | } | |
| 206 | Op::ListActionsSecrets | Op::ListActionsVariables => through::<Vec<actions::Setting>>(op, sent), | |
| 207 | Op::ListRunners => through::<Vec<g1t_contracts::runners::Runner>>(op, sent), | |
| 208 | Op::ListRunnerGroups => through::<Vec<g1t_contracts::runners::RunnerGroup>>(op, sent), | |
| 209 | Op::CreateRunnerGroup | Op::UpdateRunnerGroup => through::<g1t_contracts::runners::RunnerGroup>(op, sent), | |
| 210 | Op::GetRunnerSettings | Op::UpdateRunnerSettings => through::<g1t_contracts::runners::RunnerSettings>(op, sent), | |
| 211 | Op::CreateRunnerRegistrationToken => through::<g1t_contracts::runners::RegistrationToken>(op, sent), | |
| 212 | Op::ListWebhooks => through::<Vec<webhooks::Hook>>(op, sent), | |
| 213 | Op::ListIntegrations => through::<Vec<integrations::Connection>>(op, sent), | |
| 214 | Op::ListEvents => through::<Vec<g1t_contracts::events::Event>>(op, sent), | |
| 215 | Op::ListEmails | Op::AddEmail | Op::RemoveEmail | Op::UpdateEmailSettings => { | |
| 216 | through::<g1t_contracts::accounts::AccountEmails>(op, sent) | |
| 217 | } | |
| Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036) | 218 | Op::ConfirmEmail => through::<g1t_contracts::accounts::EmailConfirmed>(op, sent), |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 219 | Op::InviteMember | Op::RevokeWorkspaceInvite => { |
| sudo: discounts listed among what was given away | 220 | through::<g1t_contracts::identity::Invite>(op, sent) |
| 221 | } | |
| 222 | Op::ListWorkspaceInvites => through::<Vec<g1t_contracts::identity::Invite>>(op, sent), | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 223 | Op::ListInvitations => through::<Vec<g1t_contracts::identity::WorkspaceInvitation>>(op, sent), |
| API: notifications over REST and MCP, with notifications scopes | 224 | Op::ListNotifications => through::<g1t_contracts::inbox::InboxPage>(op, sent), |
| 225 | Op::GetNotificationThread | Op::MarkThreadRead | Op::MarkThreadDone | Op::SaveThread | Op::SnoozeThread => { | |
| 226 | through::<g1t_contracts::inbox::InboxThread>(op, sent) | |
| 227 | } | |
| 228 | Op::GetThreadSubscription | Op::SetThreadSubscription | Op::DeleteThreadSubscription => { | |
| 229 | through::<g1t_contracts::inbox::ThreadSubscription>(op, sent) | |
| 230 | } | |
| Merge checks: statuses and check runs on every commit | 231 | Op::Checks(ChecksOp::CreateCommitStatus) => through::<work::CommitStatus>(op, sent), |
| 232 | Op::Checks(ChecksOp::ListCommitStatuses) => through::<Vec<work::CommitStatus>>(op, sent), | |
| 233 | Op::Checks(ChecksOp::GetCombinedStatus) => through::<checks::CombinedStatus>(op, sent), | |
| 234 | Op::Checks(ChecksOp::CreateCheckRun | ChecksOp::UpdateCheckRun | ChecksOp::GetCheckRun) => { | |
| 235 | through::<checks::CommitCheckRun>(op, sent) | |
| 236 | } | |
| 237 | Op::Checks(ChecksOp::ListCheckRunAnnotations) => through::<Vec<checks::CheckAnnotation>>(op, sent), | |
| 238 | Op::Checks(ChecksOp::ListCheckRunsForRef) => through::<checks::CheckRunList>(op, sent), | |
| 239 | Op::Checks(ChecksOp::ListCheckSuitesForRef) => through::<checks::CheckSuiteList>(op, sent), | |
| 240 | Op::Checks(ChecksOp::GetCheckSuite) => through::<checks::CommitCheckSuite>(op, sent), | |
| Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae' | 241 | Op::Coordination(CoordinationOp::StartSession | CoordinationOp::UpdateSession | CoordinationOp::EndSession) => { |
| 242 | through::<work::Session>(op, sent) | |
| 243 | } | |
| 244 | Op::Coordination(CoordinationOp::GetSession) => through::<work::SessionDetail>(op, sent), | |
| 245 | Op::Coordination(CoordinationOp::ListSessions) => through::<Vec<work::Session>>(op, sent), | |
| 246 | Op::Coordination(CoordinationOp::RecordSessionEntries) => through::<work::Recorded>(op, sent), | |
| 247 | Op::Coordination(CoordinationOp::ClaimIssue) => through::<work::Claimed>(op, sent), | |
| 248 | Op::Coordination(CoordinationOp::ReleaseClaim | CoordinationOp::HandOffIssue) => through::<work::Claim>(op, sent), | |
| 249 | Op::Coordination(CoordinationOp::ListClaims) => through::<Vec<work::Claim>>(op, sent), | |
| 250 | Op::Coordination(CoordinationOp::GetWork) => through::<work::WorkContext>(op, sent), | |
| g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other. | 251 | Op::Coordination(CoordinationOp::StartAgentSession) => through::<g1t_contracts::agent_sessions::AgentSessionStarted>(op, sent), |
| sudo: discounts listed among what was given away | 252 | _ => sent, |
| 253 | } | |
| 254 | } | |
| 255 | ||
| 256 | /// Every key of `example`, as paths, outside the maps passed through. | |
| 257 | fn paths(value: &Value, path: &str, out: &mut Vec<String>) { | |
| 258 | match value { | |
| 259 | Value::Object(fields) => { | |
| 260 | for (key, value) in fields { | |
| 261 | let here = format!("{path}.{key}"); | |
| 262 | out.push(here.clone()); | |
| 263 | let user_keyed = value.is_object() | |
| 264 | && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_")); | |
| 265 | if !user_keyed { | |
| 266 | paths(value, &here, out); | |
| 267 | } | |
| 268 | } | |
| 269 | } | |
| 270 | Value::Array(items) => { | |
| 271 | for item in items { | |
| 272 | paths(item, &format!("{path}[]"), out); | |
| 273 | } | |
| 274 | } | |
| 275 | _ => {} | |
| 276 | } | |
| 277 | } | |
| 278 | ||
| 279 | #[test] | |
| 280 | fn no_route_answers_with_camel_case() { | |
| 281 | let document = document(); | |
| 282 | let (mut checked, mut converted) = (0, 0); | |
| 283 | for (path, methods) in document["paths"].as_object().unwrap() { | |
| 284 | for (method, operation) in methods.as_object().unwrap() { | |
| 285 | let example = &operation["responses"]["200"]["content"]["application/json"]["example"]; | |
| 286 | let tool = operation["x-operation"].as_str().unwrap_or_default(); | |
| 287 | let Some(op) = Op::by_name(tool) else { | |
| 288 | // Device sign-in, which is written in `snake_case` by hand. | |
| 289 | assert!(wire::camel_case_keys(example).is_empty(), "{method} {path}"); | |
| 290 | continue; | |
| 291 | }; | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 292 | let mut sample = sample(op, example); |
| 293 | // Each person gets their chosen case on the way out (people.rs). | |
| 294 | crate::people::fill(&mut sample, &std::collections::HashMap::new()); | |
| sudo: discounts listed among what was given away | 295 | converted += wire::camel_case_keys(&sample).len(); |
| 296 | let sent = wire::snake_case(sample); | |
| 297 | let leaked = wire::camel_case_keys(&sent); | |
| 298 | assert!(leaked.is_empty(), "{method} {path} sends {leaked:?}"); | |
| 299 | // The reference shows what is sent: each of its names is one. | |
| 300 | let (mut shown, mut real) = (Vec::new(), Vec::new()); | |
| 301 | paths(example, "", &mut shown); | |
| 302 | paths(&sent, "", &mut real); | |
| 303 | for name in shown { | |
| 304 | assert!(real.contains(&name), "{method} {path}: the reference shows {name}, which is not sent"); | |
| 305 | } | |
| 306 | checked += 1; | |
| 307 | } | |
| 308 | } | |
| 309 | assert!(checked >= Op::ALL.len()); | |
| 310 | // The samples are in the services' spelling, so there was something to | |
| 311 | // convert. | |
| 312 | assert!(converted > 100, "{converted}"); | |
| 313 | } | |
| 314 | ||
| 315 | #[test] | |
| 316 | fn every_route_has_a_sample() { | |
| 317 | let document = document(); | |
| 318 | for route in crate::rest::ROUTES { | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 319 | if route.no_content() { |
| 320 | continue; | |
| 321 | } | |
| sudo: discounts listed among what was given away | 322 | let path = route |
| 323 | .path | |
| 324 | .split('/') | |
| 325 | .map(|segment| match segment.strip_prefix(':') { | |
| 326 | Some(name) => format!("{{{name}}}"), | |
| 327 | None => segment.to_owned(), | |
| 328 | }) | |
| 329 | .collect::<Vec<_>>() | |
| 330 | .join("/"); | |
| 331 | let example = &document["paths"][&path][route.method.to_lowercase()]["responses"]["200"] | |
| 332 | ["content"]["application/json"]["example"]; | |
| 333 | assert!(!example.is_null(), "{} {path}", route.method); | |
| 334 | } | |
| 335 | } | |
| 336 | ||
| 337 | #[test] | |
| 338 | fn errors_and_reports_are_snake_case() { | |
| 339 | let failure = g1t_contracts::Failure { | |
| 340 | code: g1t_contracts::FailureCode::NotFound, | |
| 341 | message: "No such endpoint.".to_owned(), | |
| 342 | }; | |
| 343 | assert!(wire::camel_case_keys(&wire::snake_case(json!({ "error": failure }))).is_empty()); | |
| 344 | } | |
| 345 | ||
| 346 | #[test] | |
| 347 | fn a_job_spec_keeps_github_s_spelling() { | |
| 348 | let spec = json!({ | |
| 349 | "job": "job_1", | |
| 350 | "spec": { "runs-on": "ubuntu-latest", "timeoutMinutes": 5 }, | |
| 351 | "workflow": { "env": { "nodeEnv": "x" } }, | |
| 352 | "github": { "eventName": "push", "headRef": "" }, | |
| 353 | "event": { "pull_request": { "headSha": "x" } }, | |
| 354 | "contexts": { "inputs": { "dryRun": true }, "matrix": { "nodeVersion": 20 } }, | |
| 355 | "checkout": { "ref": "main" }, | |
| 356 | "timeoutMinutes": 30, | |
| 357 | "masks": [], | |
| 358 | }); | |
| 359 | let sent = wire::snake_case_keeping(spec.clone(), crate::JOB_SPEC_AS_GIVEN); | |
| 360 | assert_eq!(sent["timeout_minutes"], 30); | |
| 361 | assert!(sent.get("timeoutMinutes").is_none()); | |
| 362 | for kept in ["spec", "workflow", "github", "event", "contexts", "checkout"] { | |
| 363 | assert_eq!(sent[kept], spec[kept], "{kept}"); | |
| 364 | } | |
| 365 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.