| 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"], |
| 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"], |
| 65 | }, |
| 66 | Mapping { |
| 67 | command: "issue list", |
| 68 | ops: &["list_issues"], |
| 69 | }, |
| 70 | Mapping { |
| 71 | command: "issue view", |
| 72 | ops: &["get_issue"], |
| 73 | }, |
| 74 | Mapping { |
| 75 | command: "issue create", |
| 76 | ops: &["create_issue", "update_issue"], |
| 77 | }, |
| 78 | Mapping { |
| 79 | command: "issue comment", |
| 80 | ops: &["add_comment"], |
| 81 | }, |
| 82 | Mapping { |
| 83 | command: "issue close", |
| 84 | ops: &["close_issue", "add_comment"], |
| 85 | }, |
| 86 | Mapping { |
| 87 | command: "issue reopen", |
| 88 | ops: &["reopen_issue", "add_comment"], |
| 89 | }, |
| 90 | Mapping { |
| 91 | command: "issue edit", |
| 92 | ops: &["update_issue", "get_issue"], |
| 93 | }, |
| 94 | Mapping { |
| 95 | command: "pr list", |
| 96 | ops: &["list_pull_requests"], |
| 97 | }, |
| 98 | Mapping { |
| 99 | command: "pr view", |
| 100 | ops: &["get_pull_request", "list_pull_requests"], |
| 101 | }, |
| 102 | Mapping { |
| 103 | command: "pr create", |
| 104 | ops: &[ |
| 105 | "create_pull_request", |
| 106 | "get_repo", |
| 107 | "convert_pull_request_to_draft", |
| 108 | ], |
| 109 | }, |
| 110 | Mapping { |
| 111 | command: "pr checkout", |
| 112 | ops: &["get_pull_request"], |
| 113 | }, |
| 114 | Mapping { |
| 115 | command: "pr diff", |
| 116 | ops: &["get_pull_request_changes"], |
| 117 | }, |
| 118 | Mapping { |
| 119 | command: "pr checks", |
| 120 | ops: &[ |
| 121 | "get_pull_request", |
| 122 | "list_check_runs_for_ref", |
| 123 | "get_combined_status", |
| 124 | ], |
| 125 | }, |
| 126 | Mapping { |
| 127 | command: "pr review", |
| 128 | ops: &["review_pull_request", "add_comment"], |
| 129 | }, |
| 130 | Mapping { |
| 131 | command: "pr merge", |
| 132 | ops: &["merge_pull_request", "delete_branch"], |
| 133 | }, |
| 134 | Mapping { |
| 135 | command: "pr ready", |
| 136 | ops: &["mark_pull_request_ready", "convert_pull_request_to_draft"], |
| 137 | }, |
| 138 | Mapping { |
| 139 | command: "pr comment", |
| 140 | ops: &["add_comment"], |
| 141 | }, |
| 142 | Mapping { |
| 143 | command: "pr close", |
| 144 | ops: &["close_pull_request"], |
| 145 | }, |
| 146 | Mapping { |
| 147 | command: "pr reopen", |
| 148 | ops: &["reopen_pull_request"], |
| 149 | }, |
| 150 | Mapping { |
| 151 | command: "run list", |
| 152 | ops: &["list_workflow_runs"], |
| 153 | }, |
| 154 | Mapping { |
| 155 | command: "run view", |
| 156 | ops: &["get_workflow_run", "get_job_logs"], |
| 157 | }, |
| 158 | Mapping { |
| 159 | command: "run watch", |
| 160 | ops: &["get_workflow_run"], |
| 161 | }, |
| 162 | Mapping { |
| 163 | command: "run logs", |
| 164 | ops: &["get_workflow_run", "get_job_logs"], |
| 165 | }, |
| 166 | Mapping { |
| 167 | command: "run rerun", |
| 168 | ops: &["rerun_workflow_run", "rerun_failed_jobs", "rerun_job"], |
| 169 | }, |
| 170 | Mapping { |
| 171 | command: "run cancel", |
| 172 | ops: &["cancel_workflow_run"], |
| 173 | }, |
| 174 | Mapping { |
| 175 | command: "workflow list", |
| 176 | ops: &["list_workflows"], |
| 177 | }, |
| 178 | Mapping { |
| 179 | command: "workflow view", |
| 180 | ops: &["list_workflows", "get_workflow", "list_workflow_runs"], |
| 181 | }, |
| 182 | Mapping { |
| 183 | command: "workflow run", |
| 184 | ops: &["list_workflows", "dispatch_workflow", "list_workflow_runs"], |
| 185 | }, |
| 186 | Mapping { |
| 187 | command: "workflow enable", |
| 188 | ops: &["list_workflows", "enable_workflow"], |
| 189 | }, |
| 190 | Mapping { |
| 191 | command: "workflow disable", |
| 192 | ops: &["list_workflows", "disable_workflow"], |
| 193 | }, |
| 194 | Mapping { |
| 195 | command: "secret list", |
| 196 | ops: &["list_actions_secrets", "list_actions_secrets_for_workspace"], |
| 197 | }, |
| 198 | Mapping { |
| 199 | command: "secret set", |
| 200 | ops: &["set_actions_secret", "set_actions_secret_for_workspace"], |
| 201 | }, |
| 202 | Mapping { |
| 203 | command: "secret delete", |
| 204 | ops: &[ |
| 205 | "delete_actions_secret", |
| 206 | "delete_actions_secret_for_workspace", |
| 207 | ], |
| 208 | }, |
| 209 | Mapping { |
| 210 | command: "cache list", |
| 211 | ops: &["list_caches"], |
| 212 | }, |
| 213 | Mapping { |
| 214 | command: "cache usage", |
| 215 | ops: &["get_cache_usage"], |
| 216 | }, |
| 217 | Mapping { |
| 218 | command: "cache delete", |
| 219 | ops: &["delete_cache", "delete_caches_by_key"], |
| 220 | }, |
| 221 | Mapping { |
| 222 | command: "deploy list", |
| 223 | ops: &["list_deployments"], |
| 224 | }, |
| 225 | Mapping { |
| 226 | command: "deploy view", |
| 227 | ops: &["get_deployment"], |
| 228 | }, |
| 229 | Mapping { |
| 230 | command: "logs", |
| 231 | ops: &["list_logs", "list_errors", "list_projects", "get_repo"], |
| 232 | }, |
| 233 | Mapping { |
| 234 | command: "status", |
| 235 | ops: &[ |
| 236 | "search", |
| 237 | "list_notifications", |
| 238 | "get_pull_request", |
| 239 | "list_sessions", |
| 240 | "list_claims", |
| 241 | ], |
| 242 | }, |
| 243 | Mapping { command: "session start", ops: &["start_session"] }, |
| 244 | Mapping { command: "session heartbeat", ops: &["update_session"] }, |
| 245 | Mapping { command: "session log", ops: &["record_session_entries"] }, |
| 246 | Mapping { command: "session attach", ops: &["update_session"] }, |
| 247 | Mapping { command: "session end", ops: &["end_session"] }, |
| 248 | Mapping { command: "session list", ops: &["list_sessions"] }, |
| 249 | Mapping { command: "session view", ops: &["get_session"] }, |
| 250 | Mapping { command: "session touch", ops: &["record_session_entries"] }, |
| 251 | Mapping { command: "work context", ops: &["get_work"] }, |
| 252 | Mapping { command: "work claim", ops: &["claim_issue"] }, |
| 253 | Mapping { command: "work release", ops: &["release_claim"] }, |
| 254 | Mapping { command: "work handoff", ops: &["hand_off_issue"] }, |
| 255 | Mapping { command: "work list", ops: &["list_claims", "list_sessions"] }, |
| 256 | Mapping { command: "work block", ops: &["get_issue", "update_issue"] }, |
| 257 | Mapping { command: "work unblock", ops: &["get_issue", "update_issue"] }, |
| 258 | Mapping { command: "work events", ops: &["list_events"] }, |
| 259 | Mapping { command: "work dispatch", ops: &["start_agent_session"] }, |
| 260 | Mapping { command: "why", ops: &["why"] }, |
| 261 | // Every operation, by name: the table generated from the OpenAPI document. |
| 262 | Mapping { |
| 263 | command: "api", |
| 264 | ops: &[], |
| 265 | }, |
| 266 | Mapping { |
| 267 | command: "browse", |
| 268 | ops: &[], |
| 269 | }, |
| 270 | Mapping { |
| 271 | command: "alias set", |
| 272 | ops: &[], |
| 273 | }, |
| 274 | Mapping { |
| 275 | command: "alias list", |
| 276 | ops: &[], |
| 277 | }, |
| 278 | Mapping { |
| 279 | command: "alias delete", |
| 280 | ops: &[], |
| 281 | }, |
| 282 | Mapping { |
| 283 | command: "completion", |
| 284 | ops: &[], |
| 285 | }, |
| 286 | // The registry's own protocol, not the REST API. |
| 287 | Mapping { |
| 288 | command: "push", |
| 289 | ops: &[], |
| 290 | }, |
| 291 | ]; |
| 292 | |
| 293 | /// The first command that calls each operation, for `g1t api --list`. |
| 294 | pub fn by_operation() -> BTreeMap<&'static str, &'static str> { |
| 295 | let mut map = BTreeMap::new(); |
| 296 | for m in COMMANDS { |
| 297 | for op in m.ops { |
| 298 | map.entry(*op).or_insert(m.command); |
| 299 | } |
| 300 | } |
| 301 | map |
| 302 | } |
| 303 | |
| 304 | #[cfg(test)] |
| 305 | mod tests { |
| 306 | use super::*; |
| 307 | use crate::cli::Cli; |
| 308 | use clap::CommandFactory; |
| 309 | use std::collections::BTreeSet; |
| 310 | |
| 311 | fn leaves(command: &clap::Command, prefix: &str, out: &mut Vec<String>) { |
| 312 | let subs: Vec<&clap::Command> = command |
| 313 | .get_subcommands() |
| 314 | .filter(|c| c.get_name() != "help") |
| 315 | .collect(); |
| 316 | if subs.is_empty() { |
| 317 | out.push(prefix.trim().to_owned()); |
| 318 | } |
| 319 | for sub in subs { |
| 320 | leaves(sub, &format!("{prefix} {}", sub.get_name()), out); |
| 321 | } |
| 322 | } |
| 323 | |
| 324 | #[test] |
| 325 | fn every_mapped_operation_exists() { |
| 326 | for m in COMMANDS { |
| 327 | for op in m.ops { |
| 328 | assert!( |
| 329 | crate::ops::find(op).is_some(), |
| 330 | "`g1t {}` calls {op}, which the API no longer has", |
| 331 | m.command |
| 332 | ); |
| 333 | } |
| 334 | } |
| 335 | } |
| 336 | |
| 337 | #[test] |
| 338 | fn every_command_is_mapped_and_nothing_else() { |
| 339 | let mut tree = Vec::new(); |
| 340 | leaves(&Cli::command(), "", &mut tree); |
| 341 | let tree: BTreeSet<String> = tree.into_iter().collect(); |
| 342 | let mapped: BTreeSet<String> = COMMANDS.iter().map(|m| m.command.to_owned()).collect(); |
| 343 | let missing: Vec<&String> = tree.difference(&mapped).collect(); |
| 344 | let extra: Vec<&String> = mapped.difference(&tree).collect(); |
| 345 | assert!( |
| 346 | missing.is_empty(), |
| 347 | "commands with no entry in mapping.rs: {missing:?}" |
| 348 | ); |
| 349 | assert!( |
| 350 | extra.is_empty(), |
| 351 | "mapping.rs lists commands that do not exist: {extra:?}" |
| 352 | ); |
| 353 | } |
| 354 | |
| 355 | /// The operations a source file names, as string literals. |
| 356 | fn named_in(source: &str) -> BTreeSet<&'static str> { |
| 357 | let source = source.split("#[cfg(test)]").next().unwrap_or(source); |
| 358 | crate::ops::all() |
| 359 | .map(|op| op.id) |
| 360 | .filter(|id| source.contains(&format!("\"{id}\""))) |
| 361 | .collect() |
| 362 | } |
| 363 | |
| 364 | #[test] |
| 365 | fn the_table_matches_what_the_code_calls() { |
| 366 | let sources: &[(&str, &str)] = &[ |
| 367 | ("auth", include_str!("commands/auth.rs")), |
| 368 | ("repo", include_str!("commands/repo.rs")), |
| 369 | ("issue", include_str!("commands/issue.rs")), |
| 370 | ("pr", include_str!("commands/pr.rs")), |
| 371 | ("run", include_str!("commands/run.rs")), |
| 372 | ("workflow", include_str!("commands/workflow.rs")), |
| 373 | ("secret", include_str!("commands/secret.rs")), |
| 374 | ("cache", include_str!("commands/cache.rs")), |
| 375 | ("deploy", include_str!("commands/deploy.rs")), |
| 376 | ("logs", include_str!("commands/logs.rs")), |
| 377 | ("status", include_str!("commands/status.rs")), |
| 378 | ("session", include_str!("commands/session.rs")), |
| 379 | ("work", include_str!("commands/work.rs")), |
| 380 | ("why", include_str!("commands/why.rs")), |
| 381 | ]; |
| 382 | // What context.rs calls on every command's behalf. |
| 383 | let shared: BTreeSet<&str> = named_in(include_str!("context.rs")); |
| 384 | for (top, source) in sources { |
| 385 | let named = named_in(source); |
| 386 | let listed: BTreeSet<&str> = COMMANDS |
| 387 | .iter() |
| 388 | .filter(|m| m.command == *top || m.command.starts_with(&format!("{top} "))) |
| 389 | .flat_map(|m| m.ops.iter().copied()) |
| 390 | .collect(); |
| 391 | let unlisted: Vec<&&str> = named.difference(&listed).collect(); |
| 392 | let unused: Vec<&&str> = listed |
| 393 | .difference(&named) |
| 394 | .filter(|op| !shared.contains(**op)) |
| 395 | .collect(); |
| 396 | assert!( |
| 397 | unlisted.is_empty(), |
| 398 | "commands/{top}.rs calls {unlisted:?}, which mapping.rs does not list" |
| 399 | ); |
| 400 | assert!( |
| 401 | unused.is_empty(), |
| 402 | "mapping.rs lists {unused:?} for `{top}`, which commands/{top}.rs does not call" |
| 403 | ); |
| 404 | } |
| 405 | } |
| 406 | |
| 407 | /// `g1t api <operation>` can make a request for every operation: each |
| 408 | /// path parameter can be filled from a field. |
| 409 | #[test] |
| 410 | fn every_operation_is_reachable_through_api() { |
| 411 | let repo = || { |
| 412 | Ok(crate::repo::Repo { |
| 413 | host: "g1t.sh".into(), |
| 414 | owner: "acme".into(), |
| 415 | name: "web".into(), |
| 416 | }) |
| 417 | }; |
| 418 | for op in crate::ops::OPERATIONS { |
| 419 | let mut given = serde_json::Map::new(); |
| 420 | for p in op.path_params() { |
| 421 | given.insert(p.name.to_owned(), serde_json::Value::String("x".into())); |
| 422 | } |
| 423 | let plan = crate::commands::api::plan_op(op, None, given, &repo) |
| 424 | .unwrap_or_else(|e| panic!("{}: {}", op.id, e.message)); |
| 425 | assert!(!plan.path.contains('{'), "{}: {}", op.id, plan.path); |
| 426 | assert_eq!(plan.method, op.method); |
| 427 | } |
| 428 | } |
| 429 | } |