feat: add update command 9292c7ff
Steve Simkins · 2026-08-08 16:44 3 file(s) · +82 −0
src/main.rs +37 −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
src/model.rs +9 −0
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)]
tests/import_tests.rs +36 −0
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![