From 1edd3e7c3bf55613e9f35bd935735aadc18e9172 Mon Sep 17 00:00:00 2001 From: WindSoilder Date: Fri, 9 Jun 2023 21:00:40 +0800 Subject: [PATCH] add `http options` command (#9365) # Description closes: #9344 Different to other http commands, `http options` command always returns header, according to [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS), the response information is included in header. # User-Facing Changes # Tests + Formatting # After Submitting --- crates/nu-command/src/default_context.rs | 1 + crates/nu-command/src/network/http/mod.rs | 2 + crates/nu-command/src/network/http/options.rs | 196 ++++++++++++++++++ .../tests/commands/network/http/mod.rs | 1 + .../tests/commands/network/http/options.rs | 43 ++++ 5 files changed, 243 insertions(+) create mode 100644 crates/nu-command/src/network/http/options.rs create mode 100644 crates/nu-command/tests/commands/network/http/options.rs diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index c37adadc21..fbed3b3ffc 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -389,6 +389,7 @@ pub fn create_default_context() -> EngineState { HttpPatch, HttpPost, HttpPut, + HttpOptions, Url, UrlBuildQuery, UrlEncode, diff --git a/crates/nu-command/src/network/http/mod.rs b/crates/nu-command/src/network/http/mod.rs index d32039cf1b..91fcb86b5e 100644 --- a/crates/nu-command/src/network/http/mod.rs +++ b/crates/nu-command/src/network/http/mod.rs @@ -3,6 +3,7 @@ mod delete; mod get; mod head; mod http_; +mod options; mod patch; mod post; mod put; @@ -11,6 +12,7 @@ pub use delete::SubCommand as HttpDelete; pub use get::SubCommand as HttpGet; pub use head::SubCommand as HttpHead; pub use http_::Http; +pub use options::SubCommand as HttpOptions; pub use patch::SubCommand as HttpPatch; pub use post::SubCommand as HttpPost; pub use put::SubCommand as HttpPut; diff --git a/crates/nu-command/src/network/http/options.rs b/crates/nu-command/src/network/http/options.rs new file mode 100644 index 0000000000..394428586c --- /dev/null +++ b/crates/nu-command/src/network/http/options.rs @@ -0,0 +1,196 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +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_timeout, send_request, +}; + +use super::client::RequestFlags; + +#[derive(Clone)] +pub struct SubCommand; + +impl Command for SubCommand { + fn name(&self) -> &str { + "http options" + } + + fn signature(&self) -> Signature { + Signature::build("http options") + .input_output_types(vec![(Type::Nothing, Type::Any)]) + .allow_variants_without_examples(true) + .required( + "URL", + SyntaxShape::String, + "the URL to fetch the options from", + ) + .named( + "user", + SyntaxShape::Any, + "the username when authenticating", + Some('u'), + ) + .named( + "password", + SyntaxShape::Any, + "the password when authenticating", + Some('p'), + ) + .named( + "max-time", + SyntaxShape::Int, + "timeout period in seconds", + Some('m'), + ) + .named( + "headers", + SyntaxShape::Any, + "custom headers you want to add ", + Some('H'), + ) + .switch( + "insecure", + "allow insecure server connections when using SSL", + Some('k'), + ) + .switch( + "allow-errors", + "do not fail if the server returns an error code", + Some('e'), + ) + .filter() + .category(Category::Network) + } + + fn usage(&self) -> &str { + "Requests permitted communication options for a given URL." + } + + fn extra_usage(&self) -> &str { + "Performs HTTP OPTIONS operation." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["network", "fetch", "pull", "request", "curl", "wget"] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + run_get(engine_state, stack, call, input) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get options from example.com", + example: "http options https://www.example.com", + result: None, + }, + Example { + description: "Get options from example.com, with username and password", + example: "http options -u myuser -p mypass https://www.example.com", + result: None, + }, + Example { + description: "Get options from example.com, with custom header", + example: "http options -H [my-header-key my-header-value] https://www.example.com", + result: None, + }, + Example { + description: "Get options from example.com, with custom headers", + example: "http options -H [my-header-key-A my-header-value-A my-header-key-B my-header-value-B] https://www.example.com", + result: None, + }, + ] + } +} + +struct Arguments { + url: Value, + headers: Option, + insecure: bool, + user: Option, + password: Option, + timeout: Option, + allow_errors: bool, +} + +fn run_get( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, +) -> Result { + let args = Arguments { + url: call.req(engine_state, stack, 0)?, + headers: call.get_flag(engine_state, stack, "headers")?, + insecure: call.has_flag("insecure"), + user: call.get_flag(engine_state, stack, "user")?, + password: call.get_flag(engine_state, stack, "password")?, + timeout: call.get_flag(engine_state, stack, "max-time")?, + allow_errors: call.has_flag("allow-errors"), + }; + helper(engine_state, stack, call, args) +} + +// Helper function that actually goes to retrieve the resource from the url given +// The Option return a possible file extension which can be used in AutoConvert commands +fn helper( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + args: Arguments, +) -> Result { + let span = args.url.span()?; + let ctrl_c = engine_state.ctrlc.clone(); + let (requested_url, _) = http_parse_url(call, span, args.url)?; + + let client = http_client(args.insecure); + let mut request = client.request("OPTIONS", &requested_url); + + 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 = send_request(request, None, None, ctrl_c); + + // http options' response always showed in header, so we set full to true. + // And `raw` is useless too because options method doesn't return body, here we set to true + // too. + let request_flags = RequestFlags { + raw: true, + full: true, + allow_errors: args.allow_errors, + }; + + request_handle_response( + engine_state, + stack, + span, + &requested_url, + request_flags, + response, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/tests/commands/network/http/mod.rs b/crates/nu-command/tests/commands/network/http/mod.rs index 2d8f4f19a8..4a96a9c5ea 100644 --- a/crates/nu-command/tests/commands/network/http/mod.rs +++ b/crates/nu-command/tests/commands/network/http/mod.rs @@ -1,6 +1,7 @@ mod delete; mod get; mod head; +mod options; mod patch; mod post; mod put; diff --git a/crates/nu-command/tests/commands/network/http/options.rs b/crates/nu-command/tests/commands/network/http/options.rs new file mode 100644 index 0000000000..6a49a5e1c1 --- /dev/null +++ b/crates/nu-command/tests/commands/network/http/options.rs @@ -0,0 +1,43 @@ +use mockito::Server; +use nu_test_support::{nu, pipeline}; + +#[test] +fn http_options_is_success() { + let mut server = Server::new(); + + let _mock = server + .mock("OPTIONS", "/") + .with_header("Allow", "OPTIONS, GET") + .create(); + + let actual = nu!(pipeline( + format!( + r#" + http options {url} + "#, + url = server.url() + ) + .as_str() + )); + + assert!(!actual.out.is_empty()) +} + +#[test] +fn http_options_failed_due_to_server_error() { + let mut server = Server::new(); + + let _mock = server.mock("OPTIONS", "/").with_status(400).create(); + + let actual = nu!(pipeline( + format!( + r#" + http options {url} + "#, + url = server.url() + ) + .as_str() + )); + + assert!(actual.err.contains("Bad request (400)")) +}