| 1 | //! Which API operations each command calls. |
| 2 | //! |
| 3 | //! Every operation is reachable as `g1t api <operation>`, from the table |
| 4 | //! generated out of the API's OpenAPI document (`ops_table.rs`). The |
| 5 | //! commands written by hand for the common paths are listed here with the |
| 6 | //! operations they call, and the tests below hold the three together: |
| 7 | //! |
| 8 | //! - every operation named here is in the generated table, so a command |
| 9 | //! whose operation the API drops or renames fails the build's tests; |
| 10 | //! - every command in the command tree is listed here, and nothing else; |
| 11 | //! - the operations each command's source names are exactly the ones |
| 12 | //! listed for it, so this table cannot fall behind the code. |
| 13 | |
| 14 | use std::collections::BTreeMap; |
| 15 | |
| 16 | pub struct Mapping { |
| 17 | pub command: &'static str, |
| 18 | pub ops: &'static [&'static str], |
| 19 | } |
| 20 | |
| 21 | pub static COMMANDS: &[Mapping] = &[ |
| 22 | Mapping { |
| 23 | command: "auth login", |
| 24 | ops: &["device_code", "device_token", "whoami"], |
| 25 | }, |
| 26 | Mapping { |
| 27 | command: "auth logout", |
| 28 | ops: &[], |
| 29 | }, |
| 30 | Mapping { |
| 31 | command: "auth status", |
| 32 | ops: &["whoami"], |
| 33 | }, |
| 34 | Mapping { |
| 35 | command: "auth token", |
| 36 | ops: &[], |
| 37 | }, |
| 38 | Mapping { |
| 39 | command: "auth switch", |
| 40 | ops: &[], |
| 41 | }, |
| 42 | Mapping { |
| 43 | command: "auth setup-git", |
| 44 | ops: &[], |
| 45 | }, |
| 46 | Mapping { |
| 47 | command: "auth git-credential", |
| 48 | ops: &["whoami"], |
| 49 | }, |
| 50 | Mapping { |
| 51 | command: "repo view", |
| 52 | ops: &["get_repo", "list_projects"], |
| 53 | }, |
| 54 | Mapping { |
| 55 | command: "repo clone", |
| 56 | ops: &["whoami"], |
| 57 | }, |
| 58 | Mapping { |
| 59 | command: "repo create", |
| 60 | ops: &["create_repo"], |
| 61 | }, |
| 62 | Mapping { |
| 63 | command: "repo list", |
| 64 | ops: &["list_repos", "list_projects"], |
| 65 | }, |
| 66 | Mapping { |
| 67 | command: "repo fork", |
| 68 | ops: &["fork_repo"], |
| 69 | }, |
| 70 | Mapping { |
| 71 | command: "repo forks", |
| 72 | ops: &["list_forks", "list_projects"], |
| 73 | }, |
| 74 | Mapping { |
| 75 | command: "repo sync", |
| 76 | ops: &["sync_fork"], |
| 77 | }, |
| 78 | Mapping { |
| 79 | command: "repo detach", |
| 80 | ops: &["detach_fork"], |
| 81 | }, |
| 82 | Mapping { |
| 83 | command: "pr list", |
| 84 | ops: &["list_pull_requests"], |
| 85 | }, |
| 86 | Mapping { |
| 87 | command: "pr view", |
| 88 | ops: &["get_pull_request", "list_pull_requests"], |
| 89 | }, |
| 90 | Mapping { |
| 91 | command: "pr create", |
| 92 | ops: &[ |
| 93 | "create_pull_request", |
| 94 | "get_repo", |
| 95 | "convert_pull_request_to_draft", |
| 96 | "upload_attachment", |
| 97 | ], |
| 98 | }, |
| 99 | Mapping { |
| 100 | command: "pr checkout", |
| 101 | ops: &["get_pull_request"], |
| 102 | }, |
| 103 | Mapping { |
| 104 | command: "pr diff", |
| 105 | ops: &["get_pull_request_changes"], |
| 106 | }, |
| 107 | Mapping { |
| 108 | command: "pr checks", |
| 109 | ops: &[ |
| 110 | "get_pull_request", |
| 111 | "list_check_runs_for_ref", |
| 112 | "get_combined_status", |
| 113 | ], |
| 114 | }, |
| 115 | Mapping { |
| 116 | command: "pr review", |
| 117 | ops: &["review_pull_request", "add_comment"], |
| 118 | }, |
| 119 | Mapping { |
| 120 | command: "pr merge", |
| 121 | ops: &[ |
| 122 | "merge_pull_request", |
| 123 | "enable_pull_request_auto_merge", |
| 124 | "disable_pull_request_auto_merge", |
| 125 | ], |
| 126 | }, |
| 127 | Mapping { |
| 128 | command: "pr update-branch", |
| 129 | ops: &["update_pull_request_branch"], |
| 130 | }, |
| 131 | Mapping { |
| 132 | command: "pr ready", |
| 133 | ops: &["mark_pull_request_ready", "convert_pull_request_to_draft", "upload_attachment"], |
| 134 | }, |
| 135 | Mapping { |
| 136 | command: "pr comment", |
| 137 | ops: &["add_comment", "upload_attachment"], |
| 138 | }, |
| 139 | Mapping { |
| 140 | command: "pr close", |
| 141 | ops: &["close_pull_request"], |
| 142 | }, |
| 143 | Mapping { |
| 144 | command: "pr reopen", |
| 145 | ops: &["reopen_pull_request"], |
| 146 | }, |
| 147 | Mapping { |
| 148 | command: "run list", |
| 149 | ops: &["list_workflow_runs"], |
| 150 | }, |
| 151 | Mapping { |
| 152 | command: "run view", |
| 153 | ops: &["get_workflow_run", "get_job_logs"], |
| 154 | }, |
| 155 | Mapping { |
| 156 | command: "run watch", |
| 157 | ops: &["get_workflow_run"], |
| 158 | }, |
| 159 | Mapping { |
| 160 | command: "run logs", |
| 161 | ops: &["get_workflow_run", "get_job_logs"], |
| 162 | }, |
| 163 | Mapping { |
| 164 | command: "run rerun", |
| 165 | ops: &["rerun_workflow_run", "rerun_failed_jobs", "rerun_job"], |
| 166 | }, |
| 167 | Mapping { |
| 168 | command: "run cancel", |
| 169 | ops: &["cancel_workflow_run"], |
| 170 | }, |
| 171 | Mapping { |
| 172 | command: "workflow list", |
| 173 | ops: &["list_workflows"], |
| 174 | }, |
| 175 | Mapping { |
| 176 | command: "workflow view", |
| 177 | ops: &["list_workflows", "get_workflow", "list_workflow_runs"], |
| 178 | }, |
| 179 | Mapping { |
| 180 | command: "workflow run", |
| 181 | ops: &["list_workflows", "dispatch_workflow", "list_workflow_runs"], |
| 182 | }, |
| 183 | Mapping { |
| 184 | command: "workflow enable", |
| 185 | ops: &["list_workflows", "enable_workflow"], |
| 186 | }, |
| 187 | Mapping { |
| 188 | command: "workflow disable", |
| 189 | ops: &["list_workflows", "disable_workflow"], |
| 190 | }, |
| 191 | Mapping { |
| 192 | command: "secret list", |
| 193 | ops: &["list_actions_secrets", "list_actions_secrets_for_workspace"], |
| 194 | }, |
| 195 | Mapping { |
| 196 | command: "secret set", |
| 197 | ops: &["set_actions_secret", "set_actions_secret_for_workspace"], |
| 198 | }, |
| 199 | Mapping { |
| 200 | command: "secret delete", |
| 201 | ops: &[ |
| 202 | "delete_actions_secret", |
| 203 | "delete_actions_secret_for_workspace", |
| 204 | ], |
| 205 | }, |
| 206 | Mapping { |
| 207 | command: "cache list", |
| 208 | ops: &["list_caches"], |
| 209 | }, |
| 210 | Mapping { |
| 211 | command: "cache usage", |
| 212 | ops: &["get_cache_usage"], |
| 213 | }, |
| 214 | Mapping { |
| 215 | command: "cache delete", |
| 216 | ops: &["delete_cache", "delete_caches_by_key"], |
| 217 | }, |
| 218 | Mapping { |
| 219 | command: "deploy list", |
| 220 | ops: &["list_deployments"], |
| 221 | }, |
| 222 | Mapping { |
| 223 | command: "deploy view", |
| 224 | ops: &["get_deployment"], |
| 225 | }, |
| 226 | // These find the current repository's project as `logs` does. |
| 227 | Mapping { |
| 228 | command: "deploy settings", |
| 229 | ops: &["get_deployment_settings", "update_deployment_settings"], |
| 230 | }, |
| 231 | Mapping { |
| 232 | command: "deploy enable", |
| 233 | ops: &["update_deployment_settings"], |
| 234 | }, |
| 235 | Mapping { |
| 236 | command: "deploy disable", |
| 237 | ops: &["update_deployment_settings"], |
| 238 | }, |
| 239 | Mapping { |
| 240 | command: "logs", |
| 241 | ops: &["list_logs", "list_errors", "list_projects", "get_repo"], |
| 242 | }, |
| 243 | Mapping { |
| 244 | command: "status", |
| 245 | ops: &[ |
| 246 | "search", |
| 247 | "list_notifications", |
| 248 | "get_pull_request", |
| 249 | "list_sessions", |
| 250 | "list_claims", |
| 251 | ], |
| 252 | }, |
| 253 | Mapping { command: "session start", ops: &["start_session"] }, |
| 254 | Mapping { command: "session heartbeat", ops: &["update_session"] }, |
| 255 | Mapping { command: "session log", ops: &["record_session_entries"] }, |
| 256 | Mapping { command: "session attach", ops: &["update_session"] }, |
| 257 | Mapping { command: "session end", ops: &["end_session"] }, |
| 258 | Mapping { command: "session list", ops: &["list_sessions", "list_workspace_sessions"] }, |
| 259 | Mapping { command: "session view", ops: &["get_session"] }, |
| 260 | Mapping { command: "session updates", ops: &["get_session"] }, |
| 261 | Mapping { command: "session acknowledge", ops: &["record_session_entries"] }, |
| 262 | Mapping { command: "session touch", ops: &["record_session_entries"] }, |
| 263 | Mapping { command: "work list", ops: &["list_work_items"] }, |
| 264 | Mapping { command: "work view", ops: &["get_work", "get_work_item"] }, |
| 265 | Mapping { command: "work new", ops: &["open_work_item", "update_work_item", "upload_attachment", "upload_work_item_attachment"] }, |
| 266 | Mapping { command: "work comment", ops: &["add_work_item_comment", "upload_work_item_attachment"] }, |
| 267 | Mapping { command: "work edit", ops: &["update_work_item", "get_work_item"] }, |
| 268 | Mapping { command: "work close", ops: &["close_work_item", "add_work_item_comment"] }, |
| 269 | Mapping { command: "work reopen", ops: &["reopen_work_item", "add_work_item_comment"] }, |
| 270 | Mapping { command: "work link", ops: &["link_work_item"] }, |
| 271 | Mapping { command: "work unlink", ops: &["unlink_work_item"] }, |
| 272 | Mapping { command: "work claim", ops: &["claim_work_item", "claim_new_work_item"] }, |
| 273 | Mapping { command: "work release", ops: &["release_claim"] }, |
| 274 | Mapping { command: "work handoff", ops: &["hand_off_work_item"] }, |
| 275 | Mapping { command: "work status", ops: &["list_claims", "list_sessions"] }, |
| 276 | Mapping { command: "work block", ops: &["get_work_item", "update_work_item"] }, |
| 277 | Mapping { command: "work unblock", ops: &["get_work_item", "update_work_item"] }, |
| 278 | Mapping { command: "work events", ops: &["list_events"] }, |
| 279 | Mapping { command: "work plan", ops: &["get_plan", "plan_work_item"] }, |
| 280 | Mapping { command: "work next", ops: &["next_work_item", "claim_work_item"] }, |
| 281 | Mapping { command: "work dispatch", ops: &["start_agent_session", "next_work_item"] }, |
| 282 | Mapping { command: "why", ops: &["why"] }, |
| 283 | Mapping { command: "attach", ops: &["upload_attachment", "upload_work_item_attachment"] }, |
| 284 | // Every operation, by name: the table generated from the OpenAPI document. |
| 285 | Mapping { |
| 286 | command: "api", |
| 287 | ops: &[], |
| 288 | }, |
| 289 | Mapping { |
| 290 | command: "browse", |
| 291 | ops: &["resolve_ref"], |
| 292 | }, |
| 293 | Mapping { |
| 294 | command: "alias set", |
| 295 | ops: &[], |
| 296 | }, |
| 297 | Mapping { |
| 298 | command: "alias list", |
| 299 | ops: &[], |
| 300 | }, |
| 301 | Mapping { |
| 302 | command: "alias delete", |
| 303 | ops: &[], |
| 304 | }, |
| 305 | Mapping { |
| 306 | command: "completion", |
| 307 | ops: &[], |
| 308 | }, |
| 309 | // The registry's own protocol, not the REST API. |
| 310 | Mapping { |
| 311 | command: "push", |
| 312 | ops: &[], |
| 313 | }, |
| 314 | // The site's downloads, not the REST API. |
| 315 | Mapping { |
| 316 | command: "upgrade", |
| 317 | ops: &[], |
| 318 | }, |
| 319 | ]; |
| 320 | |
| 321 | /// The first command that calls each operation, for `g1t api --list`. |
| 322 | pub fn by_operation() -> BTreeMap<&'static str, &'static str> { |
| 323 | let mut map = BTreeMap::new(); |
| 324 | for m in COMMANDS { |
| 325 | for op in m.ops { |
| 326 | map.entry(*op).or_insert(m.command); |
| 327 | } |
| 328 | } |
| 329 | map |
| 330 | } |
| 331 | |
| 332 | #[cfg(test)] |
| 333 | mod tests { |
| 334 | use super::*; |
| 335 | use crate::cli::Cli; |
| 336 | use clap::CommandFactory; |
| 337 | use std::collections::BTreeSet; |
| 338 | |
| 339 | fn leaves(command: &clap::Command, prefix: &str, out: &mut Vec<String>) { |
| 340 | let subs: Vec<&clap::Command> = command |
| 341 | .get_subcommands() |
| 342 | .filter(|c| c.get_name() != "help") |
| 343 | .collect(); |
| 344 | if subs.is_empty() { |
| 345 | out.push(prefix.trim().to_owned()); |
| 346 | } |
| 347 | for sub in subs { |
| 348 | leaves(sub, &format!("{prefix} {}", sub.get_name()), out); |
| 349 | } |
| 350 | } |
| 351 | |
| 352 | #[test] |
| 353 | fn every_mapped_operation_exists() { |
| 354 | for m in COMMANDS { |
| 355 | for op in m.ops { |
| 356 | assert!( |
| 357 | crate::ops::find(op).is_some(), |
| 358 | "`g1t {}` calls {op}, which the API no longer has", |
| 359 | m.command |
| 360 | ); |
| 361 | } |
| 362 | } |
| 363 | } |
| 364 | |
| 365 | #[test] |
| 366 | fn every_command_is_mapped_and_nothing_else() { |
| 367 | let mut tree = Vec::new(); |
| 368 | leaves(&Cli::command(), "", &mut tree); |
| 369 | let tree: BTreeSet<String> = tree.into_iter().collect(); |
| 370 | let mapped: BTreeSet<String> = COMMANDS.iter().map(|m| m.command.to_owned()).collect(); |
| 371 | let missing: Vec<&String> = tree.difference(&mapped).collect(); |
| 372 | let extra: Vec<&String> = mapped.difference(&tree).collect(); |
| 373 | assert!( |
| 374 | missing.is_empty(), |
| 375 | "commands with no entry in mapping.rs: {missing:?}" |
| 376 | ); |
| 377 | assert!( |
| 378 | extra.is_empty(), |
| 379 | "mapping.rs lists commands that do not exist: {extra:?}" |
| 380 | ); |
| 381 | } |
| 382 | |
| 383 | /// The operations a source file names, as string literals. |
| 384 | fn named_in(source: &str) -> BTreeSet<&'static str> { |
| 385 | let source = source.split("#[cfg(test)]").next().unwrap_or(source); |
| 386 | crate::ops::all() |
| 387 | .map(|op| op.id) |
| 388 | .filter(|id| source.contains(&format!("\"{id}\""))) |
| 389 | .collect() |
| 390 | } |
| 391 | |
| 392 | #[test] |
| 393 | fn the_table_matches_what_the_code_calls() { |
| 394 | let sources: &[(&str, &str)] = &[ |
| 395 | ("auth", include_str!("commands/auth.rs")), |
| 396 | ("repo", include_str!("commands/repo.rs")), |
| 397 | ("pr", include_str!("commands/pr.rs")), |
| 398 | ("run", include_str!("commands/run.rs")), |
| 399 | ("workflow", include_str!("commands/workflow.rs")), |
| 400 | ("secret", include_str!("commands/secret.rs")), |
| 401 | ("cache", include_str!("commands/cache.rs")), |
| 402 | ("deploy", include_str!("commands/deploy.rs")), |
| 403 | ("logs", include_str!("commands/logs.rs")), |
| 404 | ("status", include_str!("commands/status.rs")), |
| 405 | ("session", include_str!("commands/session.rs")), |
| 406 | ("work", include_str!("commands/work.rs")), |
| 407 | ("why", include_str!("commands/why.rs")), |
| 408 | ("attach", include_str!("commands/attach.rs")), |
| 409 | ("browse", include_str!("commands/browse.rs")), |
| 410 | ]; |
| 411 | // What context.rs calls on every command's behalf, and what |
| 412 | // attach.rs uploads for --attach. |
| 413 | let shared: BTreeSet<&str> = named_in(include_str!("context.rs")) |
| 414 | .union(&named_in(include_str!("commands/attach.rs"))) |
| 415 | .copied() |
| 416 | .collect(); |
| 417 | for (top, source) in sources { |
| 418 | let named = named_in(source); |
| 419 | let listed: BTreeSet<&str> = COMMANDS |
| 420 | .iter() |
| 421 | .filter(|m| m.command == *top || m.command.starts_with(&format!("{top} "))) |
| 422 | .flat_map(|m| m.ops.iter().copied()) |
| 423 | .collect(); |
| 424 | let unlisted: Vec<&&str> = named.difference(&listed).collect(); |
| 425 | let unused: Vec<&&str> = listed |
| 426 | .difference(&named) |
| 427 | .filter(|op| !shared.contains(**op)) |
| 428 | .collect(); |
| 429 | assert!( |
| 430 | unlisted.is_empty(), |
| 431 | "commands/{top}.rs calls {unlisted:?}, which mapping.rs does not list" |
| 432 | ); |
| 433 | assert!( |
| 434 | unused.is_empty(), |
| 435 | "mapping.rs lists {unused:?} for `{top}`, which commands/{top}.rs does not call" |
| 436 | ); |
| 437 | } |
| 438 | } |
| 439 | |
| 440 | /// `g1t api <operation>` can make a request for every operation: each |
| 441 | /// path parameter can be filled from a field. |
| 442 | #[test] |
| 443 | fn every_operation_is_reachable_through_api() { |
| 444 | let repo = || { |
| 445 | Ok(crate::repo::Repo { |
| 446 | host: "g1t.sh".into(), |
| 447 | owner: "acme".into(), |
| 448 | name: "web".into(), |
| 449 | }) |
| 450 | }; |
| 451 | for op in crate::ops::OPERATIONS { |
| 452 | let mut given = serde_json::Map::new(); |
| 453 | for p in op.path_params() { |
| 454 | given.insert(p.name.to_owned(), serde_json::Value::String("x".into())); |
| 455 | } |
| 456 | let plan = crate::commands::api::plan_op(op, None, given, &repo) |
| 457 | .unwrap_or_else(|e| panic!("{}: {}", op.id, e.message)); |
| 458 | assert!(!plan.path.contains('{'), "{}: {}", op.id, plan.path); |
| 459 | assert_eq!(plan.method, op.method); |
| 460 | } |
| 461 | } |
| 462 | } |