| 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: "secret list", |
| 176 | ops: &["list_actions_secrets", "list_actions_secrets_for_workspace"], |
| 177 | }, |
| 178 | Mapping { |
| 179 | command: "secret set", |
| 180 | ops: &["set_actions_secret", "set_actions_secret_for_workspace"], |
| 181 | }, |
| 182 | Mapping { |
| 183 | command: "secret delete", |
| 184 | ops: &[ |
| 185 | "delete_actions_secret", |
| 186 | "delete_actions_secret_for_workspace", |
| 187 | ], |
| 188 | }, |
| 189 | Mapping { |
| 190 | command: "cache list", |
| 191 | ops: &["list_caches"], |
| 192 | }, |
| 193 | Mapping { |
| 194 | command: "cache usage", |
| 195 | ops: &["get_cache_usage"], |
| 196 | }, |
| 197 | Mapping { |
| 198 | command: "cache delete", |
| 199 | ops: &["delete_cache", "delete_caches_by_key"], |
| 200 | }, |
| 201 | Mapping { |
| 202 | command: "deploy list", |
| 203 | ops: &["list_deployments"], |
| 204 | }, |
| 205 | Mapping { |
| 206 | command: "deploy view", |
| 207 | ops: &["get_deployment"], |
| 208 | }, |
| 209 | Mapping { |
| 210 | command: "logs", |
| 211 | ops: &["list_logs", "list_errors", "list_projects", "get_repo"], |
| 212 | }, |
| 213 | Mapping { |
| 214 | command: "status", |
| 215 | ops: &[ |
| 216 | "search", |
| 217 | "list_notifications", |
| 218 | "get_pull_request", |
| 219 | "list_sessions", |
| 220 | "list_claims", |
| 221 | ], |
| 222 | }, |
| 223 | Mapping { command: "session start", ops: &["start_session"] }, |
| 224 | Mapping { command: "session heartbeat", ops: &["update_session"] }, |
| 225 | Mapping { command: "session log", ops: &["record_session_entries"] }, |
| 226 | Mapping { command: "session attach", ops: &["update_session"] }, |
| 227 | Mapping { command: "session end", ops: &["end_session"] }, |
| 228 | Mapping { command: "session list", ops: &["list_sessions"] }, |
| 229 | Mapping { command: "session view", ops: &["get_session"] }, |
| 230 | Mapping { command: "work context", ops: &["get_work"] }, |
| 231 | Mapping { command: "work claim", ops: &["claim_issue"] }, |
| 232 | Mapping { command: "work release", ops: &["release_claim"] }, |
| 233 | Mapping { command: "work handoff", ops: &["hand_off_issue"] }, |
| 234 | Mapping { command: "work list", ops: &["list_claims", "list_sessions"] }, |
| 235 | Mapping { command: "work block", ops: &["get_issue", "update_issue"] }, |
| 236 | Mapping { command: "work unblock", ops: &["get_issue", "update_issue"] }, |
| 237 | Mapping { command: "work events", ops: &["list_events"] }, |
| 238 | Mapping { command: "why", ops: &["why"] }, |
| 239 | // Every operation, by name: the table generated from the OpenAPI document. |
| 240 | Mapping { |
| 241 | command: "api", |
| 242 | ops: &[], |
| 243 | }, |
| 244 | Mapping { |
| 245 | command: "browse", |
| 246 | ops: &[], |
| 247 | }, |
| 248 | Mapping { |
| 249 | command: "alias set", |
| 250 | ops: &[], |
| 251 | }, |
| 252 | Mapping { |
| 253 | command: "alias list", |
| 254 | ops: &[], |
| 255 | }, |
| 256 | Mapping { |
| 257 | command: "alias delete", |
| 258 | ops: &[], |
| 259 | }, |
| 260 | Mapping { |
| 261 | command: "completion", |
| 262 | ops: &[], |
| 263 | }, |
| 264 | // The registry's own protocol, not the REST API. |
| 265 | Mapping { |
| 266 | command: "push", |
| 267 | ops: &[], |
| 268 | }, |
| 269 | ]; |
| 270 | |
| 271 | /// The first command that calls each operation, for `g1t api --list`. |
| 272 | pub fn by_operation() -> BTreeMap<&'static str, &'static str> { |
| 273 | let mut map = BTreeMap::new(); |
| 274 | for m in COMMANDS { |
| 275 | for op in m.ops { |
| 276 | map.entry(*op).or_insert(m.command); |
| 277 | } |
| 278 | } |
| 279 | map |
| 280 | } |
| 281 | |
| 282 | #[cfg(test)] |
| 283 | mod tests { |
| 284 | use super::*; |
| 285 | use crate::cli::Cli; |
| 286 | use clap::CommandFactory; |
| 287 | use std::collections::BTreeSet; |
| 288 | |
| 289 | fn leaves(command: &clap::Command, prefix: &str, out: &mut Vec<String>) { |
| 290 | let subs: Vec<&clap::Command> = command |
| 291 | .get_subcommands() |
| 292 | .filter(|c| c.get_name() != "help") |
| 293 | .collect(); |
| 294 | if subs.is_empty() { |
| 295 | out.push(prefix.trim().to_owned()); |
| 296 | } |
| 297 | for sub in subs { |
| 298 | leaves(sub, &format!("{prefix} {}", sub.get_name()), out); |
| 299 | } |
| 300 | } |
| 301 | |
| 302 | #[test] |
| 303 | fn every_mapped_operation_exists() { |
| 304 | for m in COMMANDS { |
| 305 | for op in m.ops { |
| 306 | assert!( |
| 307 | crate::ops::find(op).is_some(), |
| 308 | "`g1t {}` calls {op}, which the API no longer has", |
| 309 | m.command |
| 310 | ); |
| 311 | } |
| 312 | } |
| 313 | } |
| 314 | |
| 315 | #[test] |
| 316 | fn every_command_is_mapped_and_nothing_else() { |
| 317 | let mut tree = Vec::new(); |
| 318 | leaves(&Cli::command(), "", &mut tree); |
| 319 | let tree: BTreeSet<String> = tree.into_iter().collect(); |
| 320 | let mapped: BTreeSet<String> = COMMANDS.iter().map(|m| m.command.to_owned()).collect(); |
| 321 | let missing: Vec<&String> = tree.difference(&mapped).collect(); |
| 322 | let extra: Vec<&String> = mapped.difference(&tree).collect(); |
| 323 | assert!( |
| 324 | missing.is_empty(), |
| 325 | "commands with no entry in mapping.rs: {missing:?}" |
| 326 | ); |
| 327 | assert!( |
| 328 | extra.is_empty(), |
| 329 | "mapping.rs lists commands that do not exist: {extra:?}" |
| 330 | ); |
| 331 | } |
| 332 | |
| 333 | /// The operations a source file names, as string literals. |
| 334 | fn named_in(source: &str) -> BTreeSet<&'static str> { |
| 335 | let source = source.split("#[cfg(test)]").next().unwrap_or(source); |
| 336 | crate::ops::all() |
| 337 | .map(|op| op.id) |
| 338 | .filter(|id| source.contains(&format!("\"{id}\""))) |
| 339 | .collect() |
| 340 | } |
| 341 | |
| 342 | #[test] |
| 343 | fn the_table_matches_what_the_code_calls() { |
| 344 | let sources: &[(&str, &str)] = &[ |
| 345 | ("auth", include_str!("commands/auth.rs")), |
| 346 | ("repo", include_str!("commands/repo.rs")), |
| 347 | ("issue", include_str!("commands/issue.rs")), |
| 348 | ("pr", include_str!("commands/pr.rs")), |
| 349 | ("run", include_str!("commands/run.rs")), |
| 350 | ("secret", include_str!("commands/secret.rs")), |
| 351 | ("cache", include_str!("commands/cache.rs")), |
| 352 | ("deploy", include_str!("commands/deploy.rs")), |
| 353 | ("logs", include_str!("commands/logs.rs")), |
| 354 | ("status", include_str!("commands/status.rs")), |
| 355 | ("session", include_str!("commands/session.rs")), |
| 356 | ("work", include_str!("commands/work.rs")), |
| 357 | ("why", include_str!("commands/why.rs")), |
| 358 | ]; |
| 359 | // What context.rs calls on every command's behalf. |
| 360 | let shared: BTreeSet<&str> = named_in(include_str!("context.rs")); |
| 361 | for (top, source) in sources { |
| 362 | let named = named_in(source); |
| 363 | let listed: BTreeSet<&str> = COMMANDS |
| 364 | .iter() |
| 365 | .filter(|m| m.command == *top || m.command.starts_with(&format!("{top} "))) |
| 366 | .flat_map(|m| m.ops.iter().copied()) |
| 367 | .collect(); |
| 368 | let unlisted: Vec<&&str> = named.difference(&listed).collect(); |
| 369 | let unused: Vec<&&str> = listed |
| 370 | .difference(&named) |
| 371 | .filter(|op| !shared.contains(**op)) |
| 372 | .collect(); |
| 373 | assert!( |
| 374 | unlisted.is_empty(), |
| 375 | "commands/{top}.rs calls {unlisted:?}, which mapping.rs does not list" |
| 376 | ); |
| 377 | assert!( |
| 378 | unused.is_empty(), |
| 379 | "mapping.rs lists {unused:?} for `{top}`, which commands/{top}.rs does not call" |
| 380 | ); |
| 381 | } |
| 382 | } |
| 383 | |
| 384 | /// `g1t api <operation>` can make a request for every operation: each |
| 385 | /// path parameter can be filled from a field. |
| 386 | #[test] |
| 387 | fn every_operation_is_reachable_through_api() { |
| 388 | let repo = || { |
| 389 | Ok(crate::repo::Repo { |
| 390 | host: "g1t.sh".into(), |
| 391 | owner: "acme".into(), |
| 392 | name: "web".into(), |
| 393 | }) |
| 394 | }; |
| 395 | for op in crate::ops::OPERATIONS { |
| 396 | let mut given = serde_json::Map::new(); |
| 397 | for p in op.path_params() { |
| 398 | given.insert(p.name.to_owned(), serde_json::Value::String("x".into())); |
| 399 | } |
| 400 | let plan = crate::commands::api::plan_op(op, None, given, &repo) |
| 401 | .unwrap_or_else(|e| panic!("{}: {}", op.id, e.message)); |
| 402 | assert!(!plan.path.contains('{'), "{}: {}", op.id, plan.path); |
| 403 | assert_eq!(plan.method, op.method); |
| 404 | } |
| 405 | } |
| 406 | } |