From c4d1aa452d621d40c3b941c8879d62583917918e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Audiger?= <31616285+jaudiger@users.noreply.github.com> Date: Wed, 22 Feb 2023 13:56:11 +0100 Subject: [PATCH] Final rework for the HTTP commands (#8135) # Description Final rework for https://github.com/nushell/nushell/issues/2741, after this one, we'll add for free HTTP PUT, PATCH, DELETE and HEAD. # User-Facing Changes We can now post data using HTTP GET. I add some examples in the output of `http get --help` to demonstrate this new behavior. # Tests + Formatting Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass # After Submitting If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. --- crates/nu-command/src/network/http/client.rs | 23 ++++++ crates/nu-command/src/network/http/get.rs | 84 +++++++++++++------- crates/nu-command/src/network/http/post.rs | 78 ++++++++---------- 3 files changed, 109 insertions(+), 76 deletions(-) diff --git a/crates/nu-command/src/network/http/client.rs b/crates/nu-command/src/network/http/client.rs index aad0697d88..e87f6373fd 100644 --- a/crates/nu-command/src/network/http/client.rs +++ b/crates/nu-command/src/network/http/client.rs @@ -12,6 +12,7 @@ use std::io::BufReader; use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; +use url::Url; #[derive(PartialEq, Eq)] pub enum BodyType { @@ -30,6 +31,28 @@ pub fn http_client(allow_insecure: bool) -> reqwest::blocking::Client { .expect("Failed to build reqwest client") } +pub fn http_parse_url( + call: &Call, + span: Span, + raw_url: Value, +) -> Result<(String, Url), ShellError> { + let requested_url = raw_url.as_string()?; + let url = match url::Url::parse(&requested_url) { + Ok(u) => u, + Err(_e) => { + return Err(ShellError::UnsupportedInput( + "Incomplete or incorrect URL. Expected a full URL, e.g., https://www.example.com" + .to_string(), + format!("value: '{requested_url:?}'"), + call.head, + span, + )); + } + }; + + Ok((requested_url, url)) +} + pub fn response_to_buffer( response: blocking::Response, engine_state: &EngineState, diff --git a/crates/nu-command/src/network/http/get.rs b/crates/nu-command/src/network/http/get.rs index 82cb9f9b41..ef175ffdb8 100644 --- a/crates/nu-command/src/network/http/get.rs +++ b/crates/nu-command/src/network/http/get.rs @@ -1,7 +1,3 @@ -use crate::network::http::client::{ - http_client, request_add_authorization_header, request_add_custom_headers, - request_handle_response, request_set_timeout, -}; use nu_engine::CallExt; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; @@ -9,6 +5,11 @@ use nu_protocol::{ Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, }; +use crate::network::http::client::{ + http_client, http_parse_url, request_add_authorization_header, request_add_custom_headers, + request_handle_response, request_set_body, request_set_timeout, +}; + #[derive(Clone)] pub struct SubCommand; @@ -37,6 +38,19 @@ impl Command for SubCommand { "the password when authenticating", Some('p'), ) + .named("data", SyntaxShape::Any, "the content to post", Some('d')) + .named( + "content-type", + SyntaxShape::Any, + "the MIME type of content to post", + Some('t'), + ) + .named( + "content-length", + SyntaxShape::Any, + "the length of the content being posted", + Some('l'), + ) .named( "max-time", SyntaxShape::Int, @@ -104,18 +118,31 @@ impl Command for SubCommand { example: "http get -H [my-header-key my-header-value] https://www.example.com", result: None, }, + Example { + description: "http get from example.com, with body", + example: "http get -d 'body' https://www.example.com", + result: None, + }, + Example { + description: "http get from example.com, with JSON body", + example: "http get -t application/json -d { field: value } https://www.example.com", + result: None, + }, ] } } struct Arguments { url: Value, + headers: Option, + data: Option, + content_type: Option, + content_length: Option, raw: bool, insecure: Option, user: Option, password: Option, timeout: Option, - headers: Option, } fn run_post( @@ -126,14 +153,17 @@ fn run_post( ) -> Result { let args = Arguments { url: call.req(engine_state, stack, 0)?, + headers: call.get_flag(engine_state, stack, "headers")?, + data: call.get_flag(engine_state, stack, "data")?, + content_type: call.get_flag(engine_state, stack, "content-type")?, + content_length: call.get_flag(engine_state, stack, "content-length")?, raw: call.has_flag("raw"), insecure: call.get_flag(engine_state, stack, "insecure")?, user: call.get_flag(engine_state, stack, "user")?, password: call.get_flag(engine_state, stack, "password")?, timeout: call.get_flag(engine_state, stack, "timeout")?, - headers: call.get_flag(engine_state, stack, "headers")?, }; - helper(engine_state, stack, args) + helper(engine_state, stack, call, args) } // Helper function that actually goes to retrieve the resource from the url given @@ -141,35 +171,29 @@ fn run_post( fn helper( engine_state: &EngineState, stack: &mut Stack, + call: &Call, args: Arguments, ) -> Result { - let url_value = args.url; - let user = args.user.clone(); - let password = args.password; - let timeout = args.timeout; - let headers = args.headers; - let raw = args.raw; - - let span = url_value.span()?; - let requested_url = url_value.as_string()?; - let url = match url::Url::parse(&requested_url) { - Ok(u) => u, - Err(_e) => { - return Err(ShellError::TypeMismatch( - "Incomplete or incorrect URL. Expected a full URL, e.g., https://www.example.com" - .to_string(), - span, - )); - } - }; + let span = args.url.span()?; + let (requested_url, url) = http_parse_url(call, span, args.url)?; let client = http_client(args.insecure.is_some()); let mut request = client.get(url); - request = request_set_timeout(timeout, request)?; - request = request_add_authorization_header(user, password, request); - request = request_add_custom_headers(headers, request)?; + if let Some(data) = args.data { + request = request_set_body(args.content_type, args.content_length, data, request)?; + } + request = request_set_timeout(args.timeout, request)?; + request = request_add_authorization_header(args.user, args.password, request); + request = request_add_custom_headers(args.headers, request)?; let response = request.send().and_then(|r| r.error_for_status()); - request_handle_response(engine_state, stack, span, &requested_url, raw, response) + request_handle_response( + engine_state, + stack, + span, + &requested_url, + args.raw, + response, + ) } diff --git a/crates/nu-command/src/network/http/post.rs b/crates/nu-command/src/network/http/post.rs index a08f804644..41db2e5f68 100644 --- a/crates/nu-command/src/network/http/post.rs +++ b/crates/nu-command/src/network/http/post.rs @@ -6,7 +6,7 @@ use nu_protocol::{ }; use crate::network::http::client::{ - http_client, request_add_authorization_header, request_add_custom_headers, + http_client, http_parse_url, request_add_authorization_header, request_add_custom_headers, request_handle_response, request_set_body, request_set_timeout, }; @@ -21,8 +21,8 @@ impl Command for SubCommand { fn signature(&self) -> Signature { Signature::build("http post") .input_output_types(vec![(Type::Nothing, Type::Any)]) - .required("path", SyntaxShape::String, "the URL to post to") - .required("body", SyntaxShape::Any, "the contents of the post body") + .required("URL", SyntaxShape::String, "the URL to post to") + .required("data", SyntaxShape::Any, "the contents of the post body") .named( "user", SyntaxShape::Any, @@ -113,7 +113,7 @@ impl Command for SubCommand { result: None, }, Example { - description: "Post content to url.com with a json body", + description: "Post content to url.com, with JSON body", example: "http post -t application/json url.com { field: value }", result: None, }, @@ -122,16 +122,16 @@ impl Command for SubCommand { } struct Arguments { - path: Value, - body: Value, - timeout: Option, + url: Value, headers: Option, + data: Value, + content_type: Option, + content_length: Option, raw: bool, insecure: Option, user: Option, password: Option, - content_type: Option, - content_length: Option, + timeout: Option, } fn run_post( @@ -141,16 +141,16 @@ fn run_post( _input: PipelineData, ) -> Result { let args = Arguments { - path: call.req(engine_state, stack, 0)?, - body: call.req(engine_state, stack, 1)?, - timeout: call.get_flag(engine_state, stack, "timeout")?, + url: call.req(engine_state, stack, 0)?, headers: call.get_flag(engine_state, stack, "headers")?, - raw: call.has_flag("raw"), - user: call.get_flag(engine_state, stack, "user")?, - password: call.get_flag(engine_state, stack, "password")?, - insecure: call.get_flag(engine_state, stack, "insecure")?, + data: call.req(engine_state, stack, 1)?, content_type: call.get_flag(engine_state, stack, "content-type")?, content_length: call.get_flag(engine_state, stack, "content-length")?, + raw: call.has_flag("raw"), + insecure: call.get_flag(engine_state, stack, "insecure")?, + user: call.get_flag(engine_state, stack, "user")?, + password: call.get_flag(engine_state, stack, "password")?, + timeout: call.get_flag(engine_state, stack, "timeout")?, }; helper(engine_state, stack, call, args) } @@ -163,38 +163,24 @@ fn helper( call: &Call, args: Arguments, ) -> Result { - let url_value = args.path; - let body = args.body; - let user = args.user.clone(); - let password = args.password; - let timeout = args.timeout; - let headers = args.headers; - let content_type = args.content_type; - let content_length = args.content_length; - let raw = args.raw; + let span = args.url.span()?; + let (requested_url, url) = http_parse_url(call, span, args.url)?; - let span = url_value.span()?; - let requested_url = url_value.as_string()?; - let url = match url::Url::parse(&requested_url) { - Ok(u) => u, - Err(_e) => { - return Err(ShellError::UnsupportedInput( - "Incomplete or incorrect URL. Expected a full URL, e.g., https://www.example.com" - .to_string(), - format!("value: '{requested_url:?}'"), - call.head, - span, - )); - } - }; + let client = http_client(args.insecure.is_some()); + let mut request = client.post(url); - let mut request = http_client(args.insecure.is_some()).post(url); - - request = request_set_body(content_type, content_length, body, request)?; - request = request_set_timeout(timeout, request)?; - request = request_add_authorization_header(user, password, request); - request = request_add_custom_headers(headers, request)?; + request = request_set_body(args.content_type, args.content_length, args.data, request)?; + request = request_set_timeout(args.timeout, request)?; + request = request_add_authorization_header(args.user, args.password, request); + request = request_add_custom_headers(args.headers, request)?; let response = request.send().and_then(|r| r.error_for_status()); - request_handle_response(engine_state, stack, span, &requested_url, raw, response) + request_handle_response( + engine_state, + stack, + span, + &requested_url, + args.raw, + response, + ) }