Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add http options command #9365

Merged
merged 5 commits into from Jun 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/nu-command/src/default_context.rs
Expand Up @@ -389,6 +389,7 @@ pub fn create_default_context() -> EngineState {
HttpPatch,
HttpPost,
HttpPut,
HttpOptions,
Url,
UrlBuildQuery,
UrlEncode,
Expand Down
2 changes: 2 additions & 0 deletions crates/nu-command/src/network/http/mod.rs
Expand Up @@ -3,6 +3,7 @@ mod delete;
mod get;
mod head;
mod http_;
mod options;
mod patch;
mod post;
mod put;
Expand All @@ -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;
196 changes: 196 additions & 0 deletions 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<PipelineData, ShellError> {
run_get(engine_state, stack, call, input)
}

fn examples(&self) -> Vec<Example> {
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<Value>,
insecure: bool,
user: Option<String>,
password: Option<String>,
timeout: Option<Value>,
allow_errors: bool,
}

fn run_get(
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
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<String> 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<PipelineData, ShellError> {
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 {})
}
}
1 change: 1 addition & 0 deletions 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;
43 changes: 43 additions & 0 deletions 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)"))
}