chore: add method placeholder 221c2989
Steve Simkins · 2026-08-08 16:25 5 file(s) · +85 −10
Cargo.lock +1 −1
188 188
189 189
[[package]]
190 190
name = "cielago"
191 -
version = "0.1.0"
191 +
version = "0.1.1"
192 192
dependencies = [
193 193
 "anyhow",
194 194
 "clap",
src/app.rs +33 −6
21 21
22 22
use crate::http::{HttpResponse, OAuthToken, SendOutcome, send_with_auth, split_url_input};
23 23
use crate::model::{
24 -
    AuthKind, Collection, KeyValueRow, LabelMode, OAuthConfig, SavedRequest, variables_map,
24 +
    AuthKind, Collection, KeyValueRow, LabelMode, Method, OAuthConfig, SavedRequest, variables_map,
25 25
};
26 26
use crate::store::{self, AppConfig};
27 27
use crate::{input, ui};
753 753
                .selected_request()
754 754
                .map(|r| r.name.clone())
755 755
                .unwrap_or_default(),
756 -
            // Prefill the path only, not `base_url() + path`: re-serializing
756 +
            // Prefill `method path`, not `base_url() + path`: re-serializing
757 757
            // the full URL would rebuild the query from the table and lose each
758 758
            // row's `enabled` flag. The origin is visible in the URL bar anyway.
759 -
            // A bare `/` (what `SavedRequest::blank` gives a new request) is
760 -
            // dropped, so pasting a URL into a fresh request isn't prefixed by it.
759 +
            // The leading verb doubles as the method editor — `apply_url_input`
760 +
            // parses it back — and shows the current/default method up front. A
761 +
            // bare `/` (what `SavedRequest::blank` gives a new request) is
762 +
            // dropped, so a fresh request prefills as `GET ` awaiting a route.
761 763
            EditTarget::Url => self
762 764
                .selected_request()
763 -
                .map(|r| r.path.clone())
764 -
                .filter(|p| p != "/")
765 +
                .map(|r| {
766 +
                    let path = if r.path == "/" { "" } else { &r.path };
767 +
                    format!("{} {path}", r.method)
768 +
                })
765 769
                .unwrap_or_default(),
766 770
            EditTarget::NewRequest | EditTarget::EnvNew => String::new(),
767 771
            EditTarget::AuthField(i) => self.auth_field_value(i),
858 862
            self.status = "No request selected".into();
859 863
            return;
860 864
        };
865 +
        // A leading HTTP verb sets the method and is stripped before URL
866 +
        // parsing, so `POST /pets` fixes method and path in one edit — the same
867 +
        // single field a new request chains into after naming. A bare path
868 +
        // leaves the current method untouched. The split is on the first space
869 +
        // only, and the token must parse as a method, so a pathless `delete`
870 +
        // typed alone stays a path, not a verb.
871 +
        let mut set_method: Option<Method> = None;
872 +
        let input = match input.trim().split_once(char::is_whitespace) {
873 +
            Some((head, rest)) if !rest.trim().is_empty() => match Method::parse(head) {
874 +
                Some(m) => {
875 +
                    set_method = Some(m);
876 +
                    rest.trim()
877 +
                }
878 +
                None => input,
879 +
            },
880 +
            _ => input,
881 +
        };
861 882
        // Something that names a scheme but isn't http(s) is a typo, not a
862 883
        // relative path — say so rather than filing it under `path`. The
863 884
        // scheme-shape check matters: it keeps a stray `/api/https://…` out of
899 920
        }
900 921
901 922
        let req = &mut self.collection.requests[i];
923 +
        if let Some(m) = set_method
924 +
            && req.method != m
925 +
        {
926 +
            req.method = m;
927 +
            notes.push(format!("method → {m}"));
928 +
        }
902 929
        req.path = parts.path;
903 930
        if let Some(query) = parts.query {
904 931
            notes.push(format!("{} query param(s)", query.len()));
src/ui.rs +4 −2
510 510
                },
511 511
                EditTarget::Rename => "rename",
512 512
                EditTarget::NewRequest => "new request",
513 -
                EditTarget::Url => "url",
513 +
                EditTarget::Url => "url (verb path)",
514 514
                EditTarget::EnvNew => "server url",
515 515
                EditTarget::AuthField(_) => "auth",
516 516
            };
595 595
        Line::raw("  space        enable/disable row"),
596 596
        Line::raw("  d            delete row · m cycle method · r rename"),
597 597
        Line::raw("  p            edit URL / path (paste a full URL to set"),
598 -
        Line::raw("               the server; ?query fills the Params tab)"),
598 +
        Line::raw("               the server; ?query fills the Params tab;"),
599 +
        Line::raw("               a leading verb sets the method, e.g."),
600 +
        Line::raw("               `post /pets`)"),
599 601
        Line::raw(""),
600 602
        Line::styled("Body tab", Style::default().add_modifier(Modifier::BOLD)),
601 603
        Line::raw("  j/k, d/u     scroll · g/G top/bottom"),
tests/app_send_tests.rs +33 −0
59 59
    assert!(uuid::Uuid::parse_str(id).is_ok(), "got {id}");
60 60
}
61 61
62 +
#[test]
63 +
fn url_input_leading_verb_sets_method() {
64 +
    let mut app = app_with("http://example.com".into());
65 +
    app.select_request(0);
66 +
67 +
    // A leading verb sets the method and is stripped from the path.
68 +
    app.apply_url_input("post /pets");
69 +
    assert_eq!(app.collection.requests[0].method, Method::Post);
70 +
    assert_eq!(app.collection.requests[0].path, "/pets");
71 +
72 +
    // A bare path leaves the method untouched.
73 +
    app.apply_url_input("/pets/1");
74 +
    assert_eq!(app.collection.requests[0].method, Method::Post);
75 +
    assert_eq!(app.collection.requests[0].path, "/pets/1");
76 +
77 +
    // A lone verb with no remainder is a path, not a method.
78 +
    app.apply_url_input("delete");
79 +
    assert_eq!(app.collection.requests[0].method, Method::Post);
80 +
    assert_eq!(app.collection.requests[0].path, "/delete");
81 +
}
82 +
62 83
#[tokio::test]
63 84
async fn send_with_oauth_fetches_and_caches_token() {
64 85
    let server = MockServer::start().await;
124 145
    assert!(!app.sending);
125 146
    assert!(app.status.contains("No server configured"));
126 147
}
148 +
149 +
// After naming a new request, the flow chains into the URL edit prefilled
150 +
// with the default method so `GET ` is shown awaiting a route.
151 +
#[test]
152 +
fn new_request_chains_to_url_prefilled_with_method() {
153 +
    let mut app = app_with("http://example.com".into());
154 +
    app.start_edit(cielago::app::EditTarget::NewRequest);
155 +
    app.input.set("make thing");
156 +
    app.commit_edit();
157 +
    assert_eq!(app.editing, Some(cielago::app::EditTarget::Url));
158 +
    assert_eq!(app.input.buf, "GET ");
159 +
}
tests/ui_tests.rs +14 −1
215 215
    let mut app = test_app();
216 216
    app.start_edit(cielago::app::EditTarget::Url);
217 217
    let buf = render(&mut app, 100, 40);
218 -
    assert!(screen(&buf).contains("url> /pets"));
218 +
    assert!(screen(&buf).contains("url (verb path)> POST /pets"));
219 219
}
220 220
221 221
#[test]
232 232
    assert!(bottom.contains(":new"));
233 233
    assert!(bottom.contains(":open"));
234 234
}
235 +
236 +
#[test]
237 +
fn tmp_new_request_render_shows_get() {
238 +
    let mut app = test_app();
239 +
    app.start_edit(cielago::app::EditTarget::NewRequest);
240 +
    app.input.set("make thing");
241 +
    app.commit_edit();
242 +
    let buf = render(&mut app, 100, 40);
243 +
    let s = screen(&buf);
244 +
    let line = s.lines().find(|l| l.contains("url (verb path)")).unwrap_or("<none>");
245 +
    println!("PROMPT LINE: {:?}", line);
246 +
    assert!(s.contains("url (verb path)> GET"), "screen missing GET prefill");
247 +
}