feat: add update command
9292c7ff
3 file(s) · +82 −0
| 59 | 59 | Edit { name: String }, |
|
| 60 | 60 | /// Rename a collection (renames its file too) |
|
| 61 | 61 | Rename { name: String, new_name: String }, |
|
| 62 | + | /// Replace a collection's requests from a spec, keeping auth/vars/servers |
|
| 63 | + | Update { |
|
| 64 | + | /// Collection to update |
|
| 65 | + | name: String, |
|
| 66 | + | /// File path or http(s) URL of the spec to pull routes from |
|
| 67 | + | source: String, |
|
| 68 | + | }, |
|
| 62 | 69 | /// Show details about a collection |
|
| 63 | 70 | Info { name: String }, |
|
| 64 | 71 | /// Print the path of a collection's JSON file |
|
| 76 | 83 | Some(Command::Delete { name, force }) => cmd_delete(&name, force), |
|
| 77 | 84 | Some(Command::Edit { name }) => cmd_edit(&name), |
|
| 78 | 85 | Some(Command::Rename { name, new_name }) => cmd_rename(&name, &new_name), |
|
| 86 | + | Some(Command::Update { name, source }) => cmd_update(&name, &source).await, |
|
| 79 | 87 | Some(Command::Info { name }) => cmd_info(&name), |
|
| 80 | 88 | Some(Command::Path { name }) => cmd_path(&name), |
|
| 81 | 89 | None => cmd_open(None).await, |
|
| 307 | 315 | "Renamed \"{name}\" -> \"{new_name}\" ({})", |
|
| 308 | 316 | new_path.display() |
|
| 309 | 317 | ); |
|
| 318 | + | Ok(()) |
|
| 319 | + | } |
|
| 320 | + | ||
| 321 | + | /// Refresh a collection's routes from a spec without touching the rest of it. |
|
| 322 | + | /// Only `requests` is replaced (existing routes are overwritten); auth, |
|
| 323 | + | /// variables, servers, active server and view state stay as the user left |
|
| 324 | + | /// them. `last_request` is cleared because re-import mints new request ids, so |
|
| 325 | + | /// the old pointer would dangle. |
|
| 326 | + | async fn cmd_update(name: &str, source: &str) -> Result<()> { |
|
| 327 | + | let name = store::resolve_collection(name)?; |
|
| 328 | + | let mut collection = store::load_collection(&name)?; |
|
| 329 | + | ||
| 330 | + | let doc = openapi::load_spec(source).await?; |
|
| 331 | + | // Import under the collection's own name so the throwaway result matches; |
|
| 332 | + | // only its `requests` are pulled across. |
|
| 333 | + | let imported = openapi::import_spec(&doc, &collection.name, Some(source.to_string())); |
|
| 334 | + | ||
| 335 | + | let before = collection.requests.len(); |
|
| 336 | + | let after = imported.requests.len(); |
|
| 337 | + | collection.replace_requests_from(imported); |
|
| 338 | + | collection.spec_source = Some(source.to_string()); |
|
| 339 | + | ||
| 340 | + | let path = store::save_collection(&collection)?; |
|
| 341 | + | println!( |
|
| 342 | + | "Updated collection \"{}\" -> {}", |
|
| 343 | + | collection.name, |
|
| 344 | + | path.display() |
|
| 345 | + | ); |
|
| 346 | + | println!(" {before} -> {after} requests"); |
|
| 310 | 347 | Ok(()) |
|
| 311 | 348 | } |
|
| 312 | 349 | ||
| 106 | 106 | pub fn base_url(&self) -> Option<&str> { |
|
| 107 | 107 | self.servers.get(self.active_server).map(|s| s.as_str()) |
|
| 108 | 108 | } |
|
| 109 | + | ||
| 110 | + | /// Take the routes from a freshly imported collection, overwriting this |
|
| 111 | + | /// collection's `requests` while leaving auth, variables, servers, active |
|
| 112 | + | /// server and view state as they were. `last_request` is dropped because |
|
| 113 | + | /// import mints new request ids, so an old pointer would dangle. |
|
| 114 | + | pub fn replace_requests_from(&mut self, imported: Collection) { |
|
| 115 | + | self.requests = imported.requests; |
|
| 116 | + | self.last_request = None; |
|
| 117 | + | } |
|
| 109 | 118 | } |
|
| 110 | 119 | ||
| 111 | 120 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] |
| 235 | 235 | ); |
|
| 236 | 236 | } |
|
| 237 | 237 | ||
| 238 | + | #[tokio::test] |
|
| 239 | + | async fn update_replaces_routes_but_keeps_everything_else() { |
|
| 240 | + | let mut c = import_fixture("petstore30.yaml", "pets").await; |
|
| 241 | + | assert_eq!(c.requests.len(), 4); |
|
| 242 | + | ||
| 243 | + | // User customisations that an update must preserve. |
|
| 244 | + | c.variables |
|
| 245 | + | .push(cielago::model::KeyValueRow::new("tenant", "acme", true)); |
|
| 246 | + | let auth = c.auth.as_mut().unwrap(); |
|
| 247 | + | auth.client_id = "my-id".into(); |
|
| 248 | + | auth.client_secret = "my-secret".into(); |
|
| 249 | + | c.active_server = 1; |
|
| 250 | + | c.last_request = Some(c.requests[0].id); |
|
| 251 | + | let servers = c.servers.clone(); |
|
| 252 | + | ||
| 253 | + | // Routes come from a different spec. |
|
| 254 | + | let imported = import_fixture("api31.json", "pets").await; |
|
| 255 | + | c.replace_requests_from(imported); |
|
| 256 | + | ||
| 257 | + | // Requests were replaced wholesale by the new spec's... |
|
| 258 | + | assert_eq!(c.requests.len(), 1); |
|
| 259 | + | assert_eq!(c.requests[0].path, "/things"); |
|
| 260 | + | // ...and the now-stale selection pointer was dropped. |
|
| 261 | + | assert!(c.last_request.is_none()); |
|
| 262 | + | ||
| 263 | + | // Everything else is exactly as the user left it. |
|
| 264 | + | assert_eq!(c.servers, servers); |
|
| 265 | + | assert_eq!(c.active_server, 1); |
|
| 266 | + | let tenant = c.variables.iter().find(|v| v.key == "tenant").unwrap(); |
|
| 267 | + | assert_eq!(tenant.value, "acme"); |
|
| 268 | + | let auth = c.auth.as_ref().unwrap(); |
|
| 269 | + | assert_eq!(auth.token_url, "https://auth.pets.example.com/oauth/token"); |
|
| 270 | + | assert_eq!(auth.client_id, "my-id"); |
|
| 271 | + | assert_eq!(auth.client_secret, "my-secret"); |
|
| 272 | + | } |
|
| 273 | + | ||
| 238 | 274 | #[test] |
|
| 239 | 275 | fn variables_map_respects_enabled() { |
|
| 240 | 276 | let vars = vec![ |