> {
18 | for x in self.iter.by_ref() {
19 | match x {
20 | Ok(xx) => {
21 | if (self.predicate)(&xx) {
22 | return Some(Ok(xx));
23 | }
24 | }
25 | Err(_) => return Some(x),
26 | }
27 | }
28 | None
29 | }
30 | }
31 |
32 | pub trait FilterOkTrait {
33 | fn filter_ok(self, predicate: P) -> FilterOkIterator
34 | where
35 | Self: Sized + Iterator- >,
36 | P: FnMut(&A) -> bool,
37 | {
38 | FilterOkIterator { iter: self, predicate }
39 | }
40 | }
41 |
42 | impl FilterOkTrait for I where I: Sized + Iterator
- > {}
43 |
--------------------------------------------------------------------------------
/crates/cli/src/commands/alias/storage/load.rs:
--------------------------------------------------------------------------------
1 | use super::alias_filename;
2 | use super::{iter::FilterOkTrait, DEFAULT_ALIAS_NAME};
3 | use crate::commands::alias::{Error, Result};
4 | use crate::core::Args;
5 | use crate::shell::os::OsDirs;
6 | use std::fs::File;
7 | use std::io::{self, BufRead, BufReader};
8 | use std::path::Path;
9 |
10 | pub fn from_default(os_dirs: &OD) -> Result {
11 | from_name(os_dirs, DEFAULT_ALIAS_NAME)
12 | }
13 |
14 | pub fn from_name(os_dirs: &OD, name: &str) -> Result {
15 | match os_dirs.app_path(&alias_filename(name)) {
16 | Some(path) => match from_path(&path) {
17 | Ok(args) => Ok(args),
18 | Err(err) => Err(Error::CannotLoadAlias(name.into(), err.kind().into())),
19 | },
20 | None => Ok(Args::new()),
21 | }
22 | }
23 |
24 | pub fn from_path(path: &Path) -> std::result::Result {
25 | match File::open(&path) {
26 | Ok(file) => from_reader(&file),
27 | Err(err) => Err(err),
28 | }
29 | }
30 |
31 | fn from_reader(reader: R) -> std::result::Result {
32 | let buffer = BufReader::new(reader);
33 | match buffer.lines().filter_ok(|arg| !arg.is_empty()).collect() {
34 | Ok(args) => Ok(args),
35 | Err(err) => Err(err),
36 | }
37 | }
38 |
39 | // UNIT TESTS /////////////////////////////////////////////////////////////////////////////
40 |
41 | #[cfg(test)]
42 | mod tests {
43 | use super::*;
44 | use crate::{
45 | commands::alias::error::ErrorKind,
46 | test::{alias::*, os::TestValidOsDirs},
47 | };
48 |
49 | mod basic {
50 | use super::*;
51 |
52 | #[test]
53 | fn lines() {
54 | let args = from_reader(&b"-cushH\nX-KEY-1:val1\nX-KEY-2:val2"[..]).unwrap();
55 | assert_eq!(args, vec!["-cushH", "X-KEY-1:val1", "X-KEY-2:val2"]);
56 | }
57 |
58 | #[test]
59 | fn empty_lines() {
60 | let args = from_reader(&b"-cushH\n\n\nX-KEY-1:val1\nX-KEY-2:val2\n\n\n"[..]).unwrap();
61 | assert_eq!(args, vec!["-cushH", "X-KEY-1:val1", "X-KEY-2:val2"]);
62 | }
63 |
64 | #[test]
65 | fn error_if_invalid_characters() {
66 | let res = from_reader(&b"\xAA-cushH\nX-KEY-1:val1\nX-KEY-2:val2"[..]);
67 | assert!(res.is_err());
68 | // assert_eq!(res.unwrap_err(), Error::Config("testlines".into(), "testlines".into()));
69 | }
70 | }
71 |
72 | mod default_alias {
73 | use super::*;
74 |
75 | #[test]
76 | fn lines_from_default_alias() {
77 | setup();
78 | assert_eq!(alias_exists(DEFAULT_ALIAS_NAME), false);
79 | create_alias_file(DEFAULT_ALIAS_NAME);
80 | assert!(alias_exists(DEFAULT_ALIAS_NAME));
81 |
82 | let args = from_default(&TestValidOsDirs::new()).unwrap();
83 | assert_eq!(args, vec!["-v", "-c"]);
84 | }
85 |
86 | #[test]
87 | fn error_if_default_alias_missing() {
88 | setup();
89 |
90 | let res = from_default(&TestValidOsDirs::new());
91 | assert!(res.is_err());
92 | assert_eq!(res.unwrap_err(), Error::CannotLoadAlias(DEFAULT_ALIAS_NAME.into(), ErrorKind::AliasFileNotFound));
93 | }
94 | }
95 |
96 | mod custom_alias {
97 | use super::*;
98 |
99 | #[test]
100 | fn lines_from_empty_alias() {
101 | setup();
102 | assert_eq!(alias_exists(EMPTY_ALIAS_NAME), false);
103 | create_empty_alias_file(EMPTY_ALIAS_NAME);
104 | assert!(alias_exists(EMPTY_ALIAS_NAME));
105 |
106 | let args = from_name(&TestValidOsDirs::new(), EMPTY_ALIAS_NAME).unwrap();
107 | assert_eq!(args, Vec::::new());
108 | }
109 |
110 | #[test]
111 | fn lines_from_1arg_alias() {
112 | setup();
113 | assert_eq!(alias_exists(CUSTOM_ALIAS_NAME_1), false);
114 | create_alias_file_with_args(CUSTOM_ALIAS_NAME_1, "-cUs");
115 | assert!(alias_exists(CUSTOM_ALIAS_NAME_1));
116 |
117 | let args = from_name(&TestValidOsDirs::new(), CUSTOM_ALIAS_NAME_1).unwrap();
118 | assert_eq!(args, vec!["-cUs"]);
119 | }
120 |
121 | #[test]
122 | fn lines_from_2args_alias() {
123 | setup();
124 | assert_eq!(alias_exists(CUSTOM_ALIAS_NAME_2), false);
125 | create_alias_file_with_args(CUSTOM_ALIAS_NAME_2, "-UhH\nX-Key:Val");
126 | assert!(alias_exists(CUSTOM_ALIAS_NAME_2));
127 |
128 | let args = from_name(&TestValidOsDirs::new(), CUSTOM_ALIAS_NAME_2).unwrap();
129 | assert_eq!(args, vec!["-UhH", "X-Key:Val"]);
130 | }
131 |
132 | #[test]
133 | fn error_if_no_alias_file() {
134 | setup();
135 | let res = from_name(&TestValidOsDirs::new(), "non-existing-alias");
136 | assert!(res.is_err());
137 | assert_eq!(res.unwrap_err(), Error::CannotLoadAlias("non-existing-alias".into(), ErrorKind::AliasFileNotFound));
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/crates/cli/src/commands/alias/storage/mod.rs:
--------------------------------------------------------------------------------
1 | mod iter;
2 | pub(crate) mod load;
3 | pub(crate) mod show;
4 | pub(crate) mod store;
5 |
6 | use crate::core::Result;
7 | pub use load::from_default;
8 | pub use load::from_name;
9 |
10 | pub const DEFAULT_ALIAS_NAME: &str = "default";
11 |
12 | pub const ALIAS_FILENAME_PREFIX: &str = ".rh_";
13 | pub const ALIAS_FILENAME_SUFFIX: &str = "_rc";
14 |
15 | pub trait AliasArgItem {
16 | fn enrich_with_alias(&mut self) -> Result<()>;
17 | }
18 |
19 | fn alias_filename(name: &str) -> String {
20 | format!("{}{}{}", ALIAS_FILENAME_PREFIX, name, ALIAS_FILENAME_SUFFIX)
21 | }
22 |
--------------------------------------------------------------------------------
/crates/cli/src/commands/alias/storage/show.rs:
--------------------------------------------------------------------------------
1 | use super::load::from_path;
2 | use super::{ALIAS_FILENAME_PREFIX, ALIAS_FILENAME_SUFFIX};
3 | use crate::commands::alias::{Error, Result};
4 | use crate::core::Args;
5 | use crate::shell::os::OsDirs;
6 | use std::fs::{self, ReadDir};
7 | use std::path::Path;
8 |
9 | pub fn list(os_dirs: &OD) -> Result> {
10 | match os_dirs.app_config_directory() {
11 | Some(path) => Ok(read_app_config(&path)?
12 | .filter(|res| res.is_ok())
13 | .map(|res| res.unwrap().path())
14 | .filter(|path| path.is_file() && is_alias_file(path))
15 | .map(|alias_path| alias(&alias_path))
16 | .collect()),
17 | None => Err(Error::CannotListAlias),
18 | }
19 | }
20 |
21 | fn read_app_config(path: &Path) -> Result {
22 | match fs::read_dir(path) {
23 | Ok(res) => Ok(res),
24 | Err(_) => Err(Error::CannotListAlias),
25 | }
26 | }
27 |
28 | fn is_alias_file(path: &Path) -> bool {
29 | match path.file_name() {
30 | Some(filename) => {
31 | let filename = filename.to_str().unwrap();
32 | filename.starts_with(ALIAS_FILENAME_PREFIX) && filename.ends_with(ALIAS_FILENAME_SUFFIX)
33 | }
34 | None => false,
35 | }
36 | }
37 |
38 | fn alias(path: &Path) -> Alias {
39 | let filename = path.file_name().unwrap().to_string_lossy();
40 | let alias_start_pos_in_filename = ALIAS_FILENAME_PREFIX.len();
41 | let alias_end_pos_in_filename = filename.len() - ALIAS_FILENAME_SUFFIX.len();
42 |
43 | let args = match from_path(path) {
44 | Ok(args) => args,
45 | Err(_) => vec!["Can't load arguments".to_string()],
46 | };
47 |
48 | Alias {
49 | name: filename[alias_start_pos_in_filename..alias_end_pos_in_filename].to_string(),
50 | args,
51 | }
52 | }
53 |
54 | pub struct Alias {
55 | pub name: String,
56 | pub args: Args,
57 | }
58 |
--------------------------------------------------------------------------------
/crates/cli/src/commands/alias/storage/store.rs:
--------------------------------------------------------------------------------
1 | use super::alias_filename;
2 | use crate::commands::alias::error::ErrorKind;
3 | use crate::commands::alias::Error;
4 | use crate::commands::alias::Result;
5 | use crate::core::Args;
6 | use crate::shell::os::OsDirs;
7 | use std::fs::{self, File};
8 | use std::io::{self, BufWriter, Write};
9 | use std::path::Path;
10 |
11 | pub fn save(os_dirs: &OD, name: &str, args: &Args) -> Result<()> {
12 | match os_dirs.app_path(&alias_filename(name)) {
13 | Some(path) => match get_config_directory_error_if_any(os_dirs) {
14 | None => {
15 | let file = create_alias_file(name, &path)?;
16 | write(args, &file)
17 | }
18 | Some(err_kind) => Err(Error::CannotCreateAlias(name.to_string(), err_kind)),
19 | },
20 | None => Err(Error::CannotCreateAlias(name.to_string(), ErrorKind::ConfigDirectoryNotFound)),
21 | }
22 | }
23 |
24 | pub fn delete(os_dirs: &OD, name: &str) -> Result<()> {
25 | match os_dirs.app_path(&alias_filename(name)) {
26 | Some(path) => match get_config_directory_error_if_any(os_dirs) {
27 | None => match fs::remove_file(&path) {
28 | Ok(_) => Ok(()),
29 | Err(err) => Err(Error::CannotDeleteAlias(name.to_string(), err.kind().into())),
30 | },
31 | Some(err_kind) => Err(Error::CannotDeleteAlias(name.to_string(), err_kind)),
32 | },
33 | None => Err(Error::CannotDeleteAlias(name.to_string(), ErrorKind::ConfigDirectoryNotFound)),
34 | }
35 | }
36 |
37 | fn write(args: &Args, writer: R) -> Result<()> {
38 | let mut buffer = BufWriter::new(writer);
39 | buffer.write_all(args.join("\n").as_bytes()).expect("Unable to write data");
40 | Ok(())
41 | }
42 |
43 | fn get_config_directory_error_if_any(os_dirs: &OD) -> Option {
44 | match os_dirs.config_directory() {
45 | Some(dir) => {
46 | if Path::exists(&dir) {
47 | None
48 | } else {
49 | Some(ErrorKind::InvalidConfigDirectory)
50 | }
51 | }
52 | None => Some(ErrorKind::ConfigDirectoryNotFound),
53 | }
54 | }
55 |
56 | fn create_alias_file(name: &str, path: &Path) -> Result {
57 | match path.parent() {
58 | Some(dir) => {
59 | if !Path::exists(dir) && fs::create_dir(dir).is_err() {
60 | return Err(Error::CannotCreateAlias(name.to_string(), ErrorKind::CannotCreateAppConfigDirectory));
61 | }
62 | let file = File::create(&path)?;
63 | Ok(file)
64 | }
65 | None => Err(Error::CannotCreateAlias(name.to_string(), ErrorKind::CannotCreateAppConfigDirectory)),
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/crates/cli/src/commands/args.rs:
--------------------------------------------------------------------------------
1 | #[cfg(feature = "alias")]
2 | use super::{
3 | alias::{from_default, from_name, AliasCommand, COMMAND_ALIAS},
4 | ALIAS_NAME_PREFIX,
5 | };
6 | use super::{http::HttpCommand, ArgsCommand, Command, Result};
7 | use crate::{
8 | core::{Args, Error},
9 | shell::os::OsDirs,
10 | };
11 | use std::io::Write;
12 |
13 | impl ArgsCommand for Args {
14 | fn command(&mut self, os_dirs: &OD) -> Result>> {
15 | #[cfg(feature = "alias")]
16 | match self.first() {
17 | Some(first) => {
18 | if first == COMMAND_ALIAS {
19 | Ok(Box::new(AliasCommand {}))
20 | } else if let Some(alias_name) = first.strip_prefix(ALIAS_NAME_PREFIX) {
21 | match from_name(os_dirs, alias_name) {
22 | Ok(mut config_args) => {
23 | self.splice(..1, config_args.drain(..));
24 | Ok(Box::new(HttpCommand {}))
25 | }
26 | Err(super::alias::Error::CannotLoadAlias(alias_name, _kind)) => Err(Error::Alias(format!("{}{}", ALIAS_NAME_PREFIX, alias_name))),
27 | Err(_) => Err(Error::AliasOther), // FIXME Not good
28 | }
29 | } else {
30 | if let Ok(mut config_args) = from_default(os_dirs) {
31 | self.splice(..0, config_args.drain(..));
32 | }
33 | Ok(Box::new(HttpCommand {}))
34 | }
35 | }
36 | None => Err(Error::NoArgs),
37 | }
38 | #[cfg(not(feature = "alias"))]
39 | {
40 | if !self.is_empty() {
41 | Ok(Box::new(HttpCommand {}))
42 | } else {
43 | Err(Error::NoArgs)
44 | }
45 | }
46 | }
47 | }
48 |
49 | #[cfg(test)]
50 | mod tests {
51 | use super::*;
52 | use crate::commands::alias::storage::DEFAULT_ALIAS_NAME;
53 | use crate::test::alias::*;
54 | use crate::test::os::TestValidOsDirs;
55 |
56 | #[test]
57 | fn default_alias() {
58 | setup();
59 | assert_eq!(alias_exists(DEFAULT_ALIAS_NAME), false);
60 | create_alias_file(DEFAULT_ALIAS_NAME);
61 | assert!(alias_exists(DEFAULT_ALIAS_NAME));
62 |
63 | let mut args = rh_test::args!["-cuh", "http://test.com"];
64 | let _: Box, &mut Vec>> = args.command(&TestValidOsDirs::new()).expect("Cannot execute default_alias");
65 | assert_eq!(args, vec!["-v", "-c", "-cuh", "http://test.com"]);
66 | }
67 |
68 | #[test]
69 | fn empty_alias() {
70 | setup();
71 | assert_eq!(alias_exists(EMPTY_ALIAS_NAME), false);
72 | create_empty_alias_file(EMPTY_ALIAS_NAME);
73 | assert!(alias_exists(EMPTY_ALIAS_NAME));
74 |
75 | let mut args = rh_test::args![rh_test::arg_alias!(EMPTY_ALIAS_NAME), "-cuh", "http://test.com"];
76 | let _: Box, &mut Vec>> = args.command(&TestValidOsDirs::new()).expect("Cannot execute empty_alias");
77 | assert_eq!(args, vec!["-cuh", "http://test.com"]);
78 | }
79 |
80 | #[test]
81 | fn custom_alias_with_one_arg() {
82 | setup();
83 | assert_eq!(alias_exists(CUSTOM_ALIAS_NAME_1), false);
84 | create_alias_file_with_args(CUSTOM_ALIAS_NAME_1, "-cUs");
85 | assert!(alias_exists(CUSTOM_ALIAS_NAME_1));
86 |
87 | let mut args = rh_test::args![rh_test::arg_alias!(CUSTOM_ALIAS_NAME_1), "-cUh", "http://test.com"];
88 | let _: Box, &mut Vec>> = args.command(&TestValidOsDirs::new()).unwrap();
89 | assert_eq!(args, vec!["-cUs", "-cUh", "http://test.com"]);
90 | }
91 |
92 | #[test]
93 | fn custom_alias_with_multi_args() {
94 | setup();
95 | assert_eq!(alias_exists(CUSTOM_ALIAS_NAME_2), false);
96 | create_alias_file_with_args(CUSTOM_ALIAS_NAME_2, "-UhH\nX-Key:Val");
97 | assert!(alias_exists(CUSTOM_ALIAS_NAME_2));
98 |
99 | let mut args = rh_test::args![rh_test::arg_alias!(CUSTOM_ALIAS_NAME_2), "-cUh", "http://test.com"];
100 | let _: Box, &mut Vec>> = args.command(&TestValidOsDirs::new()).unwrap();
101 | assert_eq!(args, vec!["-UhH", "X-Key:Val", "-cUh", "http://test.com"]);
102 | }
103 |
104 | #[test]
105 | fn error_config() {
106 | let mut args = rh_test::args![rh_test::arg_alias!("error"), "-cuh", "http://test.com"];
107 | let res: Result, &mut Vec>>> = args.command(&TestValidOsDirs::new());
108 | assert!(res.is_err());
109 | // FIXME Checks the error
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/crates/cli/src/commands/debug.rs:
--------------------------------------------------------------------------------
1 | #[cfg(feature = "alias")]
2 | use crate::shell::os::OsDirs;
3 | use std::env;
4 | #[cfg(feature = "alias")]
5 | use std::path::Path;
6 |
7 | const KEY_WIDTH: usize = 25;
8 |
9 | pub fn show() {
10 | show_program();
11 | #[cfg(feature = "alias")]
12 | {
13 | println!();
14 | show_directories();
15 | }
16 | println!();
17 | show_env_vars();
18 | }
19 |
20 | fn show_program() {
21 | println!("{:width$} {}", "Name", crate::rh_name!(), width = KEY_WIDTH);
22 | println!("{:width$} {}", "Version", crate::rh_version!(), width = KEY_WIDTH);
23 | println!("{:width$} {}", "Homepage", crate::rh_homepage!(), width = KEY_WIDTH);
24 | }
25 |
26 | #[cfg(feature = "alias")]
27 | fn show_directories() {
28 | use crate::shell::os::DefaultOsDirs;
29 |
30 | let os_dirs = DefaultOsDirs::default();
31 | let mut config_dir_exists = false;
32 | match os_dirs.config_directory() {
33 | Some(path) => {
34 | println!("{:width$} {}", "Config location", path.display(), width = KEY_WIDTH);
35 | config_dir_exists = Path::new(&path).exists();
36 | if !config_dir_exists {
37 | println!("{:width$} Cannot find this directory on your platform", "", width = KEY_WIDTH);
38 | }
39 | }
40 | None => {
41 | println!("{:width$} cannot find the config path on your platform", "Config location", width = KEY_WIDTH);
42 | }
43 | };
44 |
45 | if config_dir_exists {
46 | let path = match os_dirs.app_config_directory() {
47 | Some(path) => path.display().to_string(),
48 | None => String::from("cannot find the alias path on your platform"),
49 | };
50 | println!("{:width$} {}", "Aliases location", path, width = KEY_WIDTH);
51 | } else {
52 | println!("{:width$} alias feature disabled on your platform", "Aliases location", width = KEY_WIDTH);
53 | }
54 | }
55 |
56 | fn show_env_vars() {
57 | env::vars().into_iter().for_each(|(name, value)| println!("{:width$} {}", name, value, width = KEY_WIDTH));
58 | }
59 |
--------------------------------------------------------------------------------
/crates/cli/src/commands/http/help.rs:
--------------------------------------------------------------------------------
1 | // FIXME Duplicated code with ALIAS command
2 |
3 | const LONG_FLAG_WIDTH: usize = 15;
4 | #[cfg(feature = "alias")]
5 | use crate::commands::alias::COMMAND_ALIAS;
6 | use crate::rh_name;
7 |
8 | macro_rules! newline {
9 | () => {
10 | println!("")
11 | };
12 | }
13 |
14 | macro_rules! logo {
15 | () => {
16 | println!(
17 | "╱╱╭╮
18 | ╱╱┃┃
19 | ╭━┫╰━╮
20 | ┃╭┫╭╮┃
21 | ┃┃┃┃┃┃
22 | ╰╯╰╯╰╯"
23 | )
24 | };
25 | }
26 |
27 | macro_rules! flags {
28 | ($description:expr, $long:expr) => {
29 | println!(" --{:long$} {}", $long, $description, long = LONG_FLAG_WIDTH)
30 | };
31 | ($description:expr, $long:expr, $short:expr) => {
32 | println!(" -{}, --{:long$} {}", $short, $long, $description, long = LONG_FLAG_WIDTH)
33 | };
34 | }
35 | macro_rules! key_value {
36 | ($description:expr, $long:expr) => {
37 | println!(" {:long$} {}", $long, $description, long = LONG_FLAG_WIDTH + 2)
38 | };
39 | }
40 | macro_rules! text {
41 | ($description:expr) => {
42 | println!(" {:long$} {}", "", $description, long = 3)
43 | };
44 | }
45 | macro_rules! right_text {
46 | ($description:expr) => {
47 | println!(" {:long$} {}", "", $description, long = LONG_FLAG_WIDTH + 6)
48 | };
49 | }
50 |
51 | #[cfg(feature = "alias")]
52 | macro_rules! try_help_alias {
53 | () => {
54 | right_text!(format!("try '{} {} --help' for more information", rh_name!(), COMMAND_ALIAS));
55 | };
56 | }
57 |
58 | #[cfg(feature = "alias")]
59 | macro_rules! alias {
60 | () => {
61 | println!("ALIAS:");
62 | key_value!("An alias starts with a @", "@alias");
63 | try_help_alias!();
64 | };
65 | }
66 | macro_rules! method {
67 | () => {
68 | println!("METHOD:");
69 | key_value!("If there is no data items then GET is the default method", "GET");
70 | key_value!("If there are data items then POST is the default method", "POST");
71 | key_value!("You can force any standard method (upper case)", "Standard method");
72 | right_text!("GET|POST|PUT|DELETE|HEAD|OPTIONS|CONNECT|PATCH|TRACE");
73 | key_value!("You can use a custom method (upper case)", "Custom method");
74 | };
75 | }
76 | macro_rules! options {
77 | () => {
78 | println!("OPTIONS:");
79 | flags!("Show version", "version");
80 | flags!("Show this screen", "help");
81 | flags!("Show a symbol for the request part and another one for the response part", "direction", "d");
82 | flags!("Colorize the output (shortcut: --pretty=c)", "pretty=color");
83 | flags!("Show more details, shortcut for -UHBshb", "verbose", "v");
84 | flags!("Show the request and response headers", "headers");
85 | flags!("Show the request URL and method", "url", "U");
86 | flags!("Show the request header", "req-header", "H");
87 | flags!("Show the request payload", "req-body", "B");
88 | flags!("Compact the request payload", "req-compact", "C");
89 | flags!("Show the response status and HTTP version", "status", "s");
90 | flags!("Show the response header", "header", "h");
91 | flags!("Show the response body (default)", "body", "b");
92 | flags!("Hide the response body", "body=n");
93 | flags!("Compact the response body", "compact", "c");
94 | newline!();
95 | key_value!("Combine any short flags, for example:", "-cUh...");
96 | right_text!("-c compact the response");
97 | right_text!("-U url and method");
98 | right_text!("-h response header");
99 | };
100 | }
101 | macro_rules! headers {
102 | () => {
103 | println!("HEADERS:");
104 | key_value!("List of key:value space-separated", "...");
105 | };
106 | }
107 | macro_rules! body {
108 | () => {
109 | println!("PAYLOAD:");
110 | flags!("Set the payload and don't apply any transformation", "raw=");
111 | flags!("Force the 'Accept' header to 'application/json' (default)", "json");
112 | flags!("Set the 'Content-Type' and serialize data items as form URL encoded", "form");
113 | key_value!("Data items as a list of key=value space-separated", "...");
114 | right_text!("Data items are converted to JSON (default) or URL encoded (--form)");
115 | };
116 | }
117 |
118 | #[cfg(feature = "alias")]
119 | macro_rules! subcommand {
120 | () => {
121 | println!("SUBCOMMAND:");
122 | key_value!("Manage aliases", COMMAND_ALIAS);
123 | try_help_alias!();
124 | };
125 | }
126 |
127 | macro_rules! thanks {
128 | () => {
129 | println!("Thanks for using {}!", rh_name!())
130 | };
131 | }
132 |
133 | pub fn show() {
134 | logo!();
135 |
136 | newline!();
137 | println!("USAGE:");
138 | #[cfg(feature = "alias")]
139 | {
140 | text!(format!("{} [@alias] [METHOD] url [options] [headers] [payload]", rh_name!()));
141 | }
142 | #[cfg(not(feature = "alias"))]
143 | {
144 | text!(format!("{} [METHOD] url [options] [headers] [payload]", rh_name!()));
145 | }
146 | text!(format!("{} --help | -h", rh_name!()));
147 | text!(format!("{} --version", rh_name!()));
148 | #[cfg(feature = "alias")]
149 | {
150 | newline!();
151 | text!(format!("{} [SUBCOMMAND] [options]", rh_name!()));
152 | text!(format!("{} [SUBCOMMAND] --help | -h", rh_name!()));
153 | }
154 |
155 | #[cfg(feature = "alias")]
156 | {
157 | newline!();
158 | alias!();
159 | }
160 | newline!();
161 | method!();
162 | newline!();
163 | options!();
164 | newline!();
165 | headers!();
166 | newline!();
167 | body!();
168 | newline!();
169 | #[cfg(feature = "alias")]
170 | {
171 | subcommand!();
172 | newline!();
173 | }
174 | thanks!();
175 | }
176 |
--------------------------------------------------------------------------------
/crates/cli/src/commands/http/mod.rs:
--------------------------------------------------------------------------------
1 | mod help;
2 | mod output;
3 | mod render;
4 | mod version;
5 |
6 | use super::debug;
7 | use super::{Command, DonePtr, Result};
8 | use crate::core::Args;
9 | use crate::core::Mode;
10 | use crate::parser;
11 | use crate::request;
12 | use crate::shell::os::OsDirs;
13 | use crate::shell::Shell;
14 | use std::io::Write;
15 |
16 | pub struct HttpCommand;
17 |
18 | impl Command for HttpCommand {
19 | fn execute(&self, shell: &mut Shell, args: &mut Args, _: DonePtr) -> Result<()> {
20 | let ws = parser::execute(args)?;
21 | match ws.mode() {
22 | Mode::Help => help::show(),
23 | Mode::Version => version::show(),
24 | Mode::Debug => debug::show(),
25 | Mode::Run => {
26 | {
27 | let mut headers = ws.headers.borrow_mut();
28 | request::headers::upgrade(&ws, &mut headers);
29 | }
30 | let headers = ws.headers.borrow();
31 | let req_number = 0u8;
32 | let response = request::execute(&ws, req_number, &headers)?;
33 | output::render(shell, &ws, req_number, response)?;
34 | }
35 | }
36 | Ok(())
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/crates/cli/src/commands/http/output.rs:
--------------------------------------------------------------------------------
1 | use super::render::RequestRender;
2 | use super::render::ResponseRender;
3 | use crate::core::Result;
4 | use crate::core::Workspace;
5 | use crate::request::Response;
6 | use crate::shell::os::OsDirs;
7 | use crate::shell::Shell;
8 | use std::borrow::Borrow;
9 | use std::cell::RefCell;
10 | use std::io;
11 | use std::io::Read;
12 | use std::io::Write;
13 |
14 | pub fn render(shell: &mut Shell, ws: &Workspace, _req_number: u8, response: Response) -> Result<()> {
15 | if ws.output_redirected && !ws.flags.borrow().use_color {
16 | render_raw_content(ws, RefCell::new(response))?;
17 | } else {
18 | let style_enabled = shell.enable_colors();
19 |
20 | let headers = ws.headers.borrow();
21 | let rf = RequestRender::new(ws, &headers, ws.theme.as_ref(), style_enabled);
22 | shell.out(rf)?;
23 |
24 | let rf = ResponseRender::new(ws, RefCell::new(response), ws.theme.as_ref(), style_enabled);
25 | shell.out(rf)?;
26 | }
27 | Ok(())
28 | }
29 |
30 | fn render_raw_content(_args: &Workspace, response: RefCell) -> io::Result<()> {
31 | let mut bytes = Vec::new();
32 | let mut response = response.borrow_mut();
33 | response.read_to_end(&mut bytes)?;
34 | io::stdout().write_all(&bytes)
35 | }
36 |
--------------------------------------------------------------------------------
/crates/cli/src/commands/http/render/header.rs:
--------------------------------------------------------------------------------
1 | use super::{HeaderRender, Render};
2 | use crate::request::header::StandardHeader;
3 | use crate::request::HeaderMap;
4 | use crate::theme::HeaderTheme;
5 | use crate::{core::Workspace, theme::DirectionTheme};
6 | use std::io::{Result, Write};
7 |
8 | impl<'a> HeaderRender<'a> {
9 | pub fn new(
10 | workspace: &'a Workspace,
11 | headers: &'a HeaderMap,
12 | header_theme: &'a dyn HeaderTheme,
13 | direction_theme: &'a dyn DirectionTheme,
14 | direction_symbol: &'a [u8],
15 | style_enabled: bool,
16 | ) -> Self {
17 | Self {
18 | workspace,
19 | headers,
20 | header_theme,
21 | direction_theme,
22 | direction_symbol,
23 | style_enabled,
24 | }
25 | }
26 | }
27 |
28 | impl<'a> Render for HeaderRender<'a> {
29 | #[inline]
30 | fn write(&self, writer: &mut W) -> Result<()>
31 | where
32 | W: Write,
33 | {
34 | let flags = self.workspace.flags;
35 | let header_theme = self.header_theme;
36 |
37 | for (key, value) in self.headers.iter() {
38 | let is_standard = key.is_standard();
39 | let key_style = header_theme.header_name(is_standard);
40 | let key = key.as_str();
41 |
42 | if flags.show_direction {
43 | self.write_direction(writer, is_standard)?;
44 | }
45 | self.write_with_style(writer, key.as_bytes(), &key_style)?;
46 | self.write_with_style(writer, ": ".as_bytes(), &key_style)?;
47 | self.write_with_style(writer, value.to_str().unwrap_or("No value").as_bytes(), &header_theme.header_value(is_standard))?;
48 | self.write_newline(writer)?;
49 | }
50 | Ok(())
51 | }
52 |
53 | #[inline]
54 | fn is_style_active(&self) -> bool {
55 | self.style_enabled
56 | }
57 | }
58 |
59 | impl<'a> HeaderRender<'a> {
60 | #[inline]
61 | fn write_direction(&self, writer: &mut W, is_standard: bool) -> Result<()> {
62 | self.write_with_style(writer, self.direction_symbol, &self.direction_theme.direction(is_standard))
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/crates/cli/src/commands/http/render/mod.rs:
--------------------------------------------------------------------------------
1 | mod header;
2 | mod request;
3 | mod response;
4 |
5 | use crate::core::Workspace;
6 | use crate::request::{HeaderMap, Response};
7 | use crate::shell::Render;
8 | use crate::theme::{DirectionTheme, HeaderTheme, Theme};
9 | use std::cell::RefCell;
10 |
11 | pub const DIRECTION_REQUEST: &[u8] = b"> ";
12 | pub const DIRECTION_RESPONSE: &[u8] = b"< ";
13 |
14 | pub struct RequestRender<'a> {
15 | workspace: &'a Workspace,
16 | headers: &'a HeaderMap,
17 | theme: &'a dyn Theme,
18 | style_enabled: bool,
19 | req_number: usize,
20 | }
21 |
22 | pub struct ResponseRender<'a> {
23 | workspace: &'a Workspace,
24 | response: RefCell,
25 | theme: &'a dyn Theme,
26 | style_enabled: bool,
27 | }
28 |
29 | pub struct HeaderRender<'a> {
30 | workspace: &'a Workspace,
31 | headers: &'a HeaderMap,
32 | header_theme: &'a dyn HeaderTheme,
33 | direction_theme: &'a dyn DirectionTheme,
34 | direction_symbol: &'a [u8],
35 | style_enabled: bool,
36 | }
37 |
--------------------------------------------------------------------------------
/crates/cli/src/commands/http/render/request.rs:
--------------------------------------------------------------------------------
1 | use super::{HeaderRender, Render, RequestRender, DIRECTION_REQUEST};
2 | use crate::items::Items;
3 | use crate::request::HeaderMap;
4 | use crate::shell::form::FormRender;
5 | use crate::shell::json::JsonRender;
6 | use crate::{
7 | core::{Workspace, WorkspaceData},
8 | theme::Theme,
9 | };
10 | use std::io::{Result, Write};
11 |
12 | impl<'a> RequestRender<'a> {
13 | pub fn new(workspace: &'a Workspace, headers: &'a HeaderMap, theme: &'a dyn Theme, style_enabled: bool) -> Self {
14 | Self {
15 | workspace,
16 | headers,
17 | theme,
18 | style_enabled,
19 | req_number: 0,
20 | }
21 | }
22 | }
23 |
24 | impl<'a> Render for RequestRender<'a> {
25 | #[inline]
26 | fn write(&self, writer: &mut W) -> Result<()>
27 | where
28 | W: Write,
29 | {
30 | let ws = self.workspace;
31 | let flags = ws.flags;
32 | if flags.show_request_url {
33 | if flags.show_direction {
34 | self.write_direction(writer, true)?;
35 | }
36 | self.write_method(writer)?;
37 | writer.write_all(b" ")?;
38 | self.write_url(writer)?;
39 | self.write_newline(writer)?;
40 | }
41 | if flags.show_request_headers {
42 | self.write_headers(writer)?;
43 | }
44 | if flags.show_request_body {
45 | self.write_body(writer)?;
46 | }
47 | Ok(())
48 | }
49 |
50 | #[inline]
51 | fn is_style_active(&self) -> bool {
52 | self.style_enabled
53 | }
54 | }
55 |
56 | impl<'a> RequestRender<'a> {
57 | #[inline]
58 | fn write_direction(&self, writer: &mut W, is_standard: bool) -> Result<()> {
59 | self.write_with_style(writer, DIRECTION_REQUEST, &self.theme.request().direction(is_standard))
60 | }
61 |
62 | #[inline]
63 | fn write_method(&self, writer: &mut W) -> Result<()> {
64 | let ws = self.workspace;
65 | self.write_with_style(writer, ws.method.as_str().as_bytes(), &self.theme.request().method())
66 | }
67 |
68 | #[inline]
69 | fn write_url(&self, writer: &mut W) -> Result<()> {
70 | let ws = self.workspace;
71 | let urls: &[String] = &ws.urls;
72 |
73 | self.write_with_style(
74 | writer,
75 | if urls.len() > self.req_number { &urls[self.req_number] } else { "??" }.as_bytes(),
76 | &self.theme.request().url(),
77 | )
78 | }
79 |
80 | #[inline]
81 | fn write_body(&self, writer: &mut W) -> Result<()> {
82 | let ws = self.workspace;
83 | if ws.has_items() {
84 | let flags = ws.flags;
85 | let items = ws.items.borrow();
86 | if ws.is_json() {
87 | let json_render = JsonRender::new(&items as &Items, flags.show_request_compact, self.style_enabled);
88 | json_render.write(writer)?;
89 | } else {
90 | let json_render = FormRender::new(&items as &Items, flags.show_request_compact, self.style_enabled);
91 | json_render.write(writer)?;
92 | }
93 | self.write_newline(writer)?;
94 | } else if let Some(ref raw) = ws.raw {
95 | writer.write_all(raw.as_bytes())?;
96 | self.write_newline(writer)?;
97 | }
98 | Ok(())
99 | }
100 |
101 | #[inline]
102 | fn write_headers(&self, writer: &mut W) -> Result<()> {
103 | let request_theme = self.theme.request();
104 | let header_theme = request_theme.as_header();
105 | let direction_theme = request_theme.as_direction();
106 | let header_render = HeaderRender::new(self.workspace, self.headers, header_theme, direction_theme, DIRECTION_REQUEST, self.style_enabled);
107 | header_render.write(writer)
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/crates/cli/src/commands/http/render/response.rs:
--------------------------------------------------------------------------------
1 | use super::{HeaderRender, Render, ResponseRender, DIRECTION_RESPONSE};
2 | use crate::request::Response;
3 | use crate::rh_name;
4 | use crate::shell::json::JsonRender;
5 | use crate::{core::Workspace, theme::Theme};
6 | use content_inspector::inspect;
7 | use serde_json::Value;
8 | use std::cell::RefCell;
9 | use std::io::Read;
10 | use std::io::{Result, Write};
11 |
12 | impl<'a> ResponseRender<'a> {
13 | pub fn new(workspace: &'a Workspace, response: RefCell, theme: &'a dyn Theme, style_enabled: bool) -> Self {
14 | Self {
15 | workspace,
16 | response,
17 | theme,
18 | style_enabled,
19 | }
20 | }
21 | }
22 |
23 | impl<'a> Render for ResponseRender<'a> {
24 | #[inline]
25 | fn write(&self, writer: &mut W) -> Result<()>
26 | where
27 | W: Write,
28 | {
29 | let ws = self.workspace;
30 | let flags = ws.flags;
31 | if flags.show_response_status {
32 | if flags.show_direction {
33 | self.write_direction(writer, true)?;
34 | }
35 | self.write_version_and_status(writer)?;
36 | }
37 | if flags.show_response_headers {
38 | self.write_headers(writer)?;
39 | }
40 | if flags.show_response_body {
41 | self.write_body(writer)?;
42 | }
43 | Ok(())
44 | }
45 |
46 | #[inline]
47 | fn is_style_active(&self) -> bool {
48 | self.style_enabled
49 | }
50 | }
51 |
52 | impl<'a> ResponseRender<'a> {
53 | #[inline]
54 | fn write_direction(&self, writer: &mut W, is_standard: bool) -> Result<()> {
55 | self.write_with_style(writer, DIRECTION_RESPONSE, &self.theme.response().direction(is_standard))
56 | }
57 |
58 | #[inline]
59 | fn write_version_and_status(&self, writer: &mut W) -> Result<()> {
60 | let response = &self.response.borrow();
61 | let status = response.status();
62 | let theme = self.workspace.theme.response();
63 |
64 | let style = theme.version();
65 | let message = format!("{:?} ", response.version());
66 | self.write_with_style(writer, message.as_bytes(), &style)?;
67 |
68 | let style = theme.status();
69 | self.write_with_style(writer, status.as_str().as_bytes(), &style)?;
70 | writer.write_all(b" ")?;
71 | self.write_with_style(writer, status.canonical_reason().unwrap_or("Unknown").as_bytes(), &style)?;
72 | self.write_newline(writer)
73 | }
74 |
75 | #[inline]
76 | fn write_body(&self, writer: &mut W) -> Result<()> {
77 | let ws = self.workspace;
78 | let flags = ws.flags;
79 | let mut response = self.response.borrow_mut();
80 |
81 | let mut bytes = Vec::new();
82 | let size = response.read_to_end(&mut bytes).unwrap_or(0);
83 | let content_type = inspect(&bytes);
84 | if content_type.is_binary() {
85 | self.write_binary_usage(writer, size)?;
86 | } else {
87 | let body = String::from_utf8_lossy(&bytes);
88 | match serde_json::from_str::(&body) {
89 | Ok(json) => {
90 | let json_render = JsonRender::new(&json, flags.show_response_compact, self.style_enabled);
91 | json_render.write(writer)?;
92 | }
93 | Err(_) => {
94 | writer.write_all(body.as_bytes())?;
95 | }
96 | }
97 | }
98 | self.write_newline(writer)
99 | }
100 |
101 | #[inline]
102 | fn write_binary_usage(&self, writer: &mut W, size: usize) -> Result<()> {
103 | let message = format!(
104 | "Binary data not shown in terminal\nContent size {}b\nTo copy the content in a file, you should try:\n{} > filename",
105 | size,
106 | rh_name!()
107 | );
108 | writer.write_all(message.as_bytes())?;
109 | self.write_newline(writer)?;
110 | Ok(())
111 | }
112 |
113 | #[inline]
114 | fn write_headers(&self, writer: &mut W) -> Result<()> {
115 | let response = self.response.borrow();
116 | let headers = response.headers();
117 | let response_theme = self.theme.response();
118 | let header_theme = response_theme.as_header();
119 | let direction_theme = response_theme.as_direction();
120 | let header_render = HeaderRender::new(self.workspace, headers, header_theme, direction_theme, DIRECTION_RESPONSE, self.style_enabled);
121 | header_render.write(writer)
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/crates/cli/src/commands/http/version.rs:
--------------------------------------------------------------------------------
1 | pub fn show() {
2 | println!("{}", crate::rh_version!());
3 | }
4 |
--------------------------------------------------------------------------------
/crates/cli/src/commands/mod.rs:
--------------------------------------------------------------------------------
1 | #[cfg(feature = "alias")]
2 | pub(crate) mod alias;
3 | pub(crate) mod args;
4 | mod debug;
5 | pub(crate) mod http;
6 |
7 | use crate::{
8 | core::{Args, Result},
9 | shell::Shell,
10 | };
11 |
12 | type DonePtr = fn();
13 |
14 | #[cfg(feature = "alias")]
15 | const ALIAS_NAME_PREFIX: char = '@';
16 |
17 | pub trait ArgsCommand {
18 | fn command(&mut self, os_dirs: &OD) -> Result>>;
19 | }
20 |
21 | pub trait Command {
22 | fn execute(&self, shell: &mut Shell, args: &mut Args, done: DonePtr) -> Result<()>;
23 | }
24 |
--------------------------------------------------------------------------------
/crates/cli/src/core/error.rs:
--------------------------------------------------------------------------------
1 | #[cfg(feature = "alias")]
2 | use crate::commands::alias::Error as AliasError;
3 |
4 | #[cfg_attr(test, derive(Debug))]
5 | #[derive(PartialEq)]
6 | pub enum Error {
7 | NoArgs,
8 | MissingUrl,
9 | ItemsAndRawMix,
10 | TooManyRaw,
11 | ContradictoryScheme,
12 | Unexpected(String),
13 | InvalidFlag(String),
14 | InvalidHeader(String),
15 | InvalidItem(String),
16 | BadHeaderName(String),
17 | BadHeaderValue(String),
18 | Request(String),
19 | Io(String),
20 | #[cfg(feature = "alias")]
21 | AliasCommand(AliasError),
22 | #[cfg(feature = "alias")]
23 | Alias(String),
24 | #[cfg(feature = "alias")]
25 | AliasOther, // FIXME To be removed
26 | }
27 |
--------------------------------------------------------------------------------
/crates/cli/src/core/flags.rs:
--------------------------------------------------------------------------------
1 | #[cfg_attr(test, derive(Debug))]
2 | #[derive(Clone, Copy)]
3 | pub struct Flags {
4 | pub show_version: bool,
5 | pub show_help: bool,
6 | pub show_short_help: bool,
7 | pub debug: bool,
8 |
9 | pub https: bool,
10 | pub http: bool,
11 | pub use_color: bool,
12 | pub show_direction: bool,
13 |
14 | pub as_json: bool,
15 | pub as_form: bool,
16 |
17 | pub show_request_url: bool,
18 | pub show_request_headers: bool,
19 | pub show_request_compact: bool,
20 | pub show_request_body: bool,
21 |
22 | pub show_response_status: bool,
23 | pub show_response_headers: bool,
24 | pub show_response_compact: bool,
25 | pub show_response_body: bool,
26 | }
27 |
--------------------------------------------------------------------------------
/crates/cli/src/core/mod.rs:
--------------------------------------------------------------------------------
1 | mod error;
2 | mod flags;
3 | mod types;
4 | mod workspace;
5 |
6 | pub use error::Error;
7 | pub use flags::Flags;
8 | pub use types::{Args, HeaderMap, Mode, Result};
9 | pub use workspace::Workspace;
10 |
11 | pub trait PushDataItem {
12 | fn push(&mut self, item: &str) -> Result<()>;
13 | }
14 |
15 | pub trait WorkspaceData {
16 | fn is_json(&self) -> bool;
17 | fn is_form(&self) -> bool;
18 | fn has_items(&self) -> bool;
19 | }
20 |
--------------------------------------------------------------------------------
/crates/cli/src/core/types.rs:
--------------------------------------------------------------------------------
1 | use super::error::Error;
2 |
3 | pub type Args = Vec;
4 | pub type HeaderMap = reqwest::header::HeaderMap;
5 | pub type Result = std::result::Result;
6 |
7 | #[cfg_attr(test, derive(Debug, PartialEq))]
8 | pub enum Mode {
9 | Run,
10 | Help,
11 | Version,
12 | Debug,
13 | }
14 |
--------------------------------------------------------------------------------
/crates/cli/src/core/workspace.rs:
--------------------------------------------------------------------------------
1 | use crate::items::Items;
2 | use crate::request::Method;
3 | use crate::theme::Theme;
4 | use std::cell::RefCell;
5 |
6 | use super::{Flags, HeaderMap, Mode};
7 |
8 | #[cfg_attr(test, derive(Debug))]
9 | pub struct Workspace {
10 | pub method: Method,
11 | pub urls: Vec,
12 | pub output_redirected: bool,
13 | pub terminal_columns: u16,
14 | pub theme: Box, // FIXME Create a crate for theme
15 | pub flags: Flags,
16 | pub headers: RefCell,
17 | pub items: RefCell,
18 | pub raw: Option,
19 | pub certificate_authority_file: Option,
20 | }
21 |
22 | impl Workspace {
23 | pub fn mode(&self) -> Mode {
24 | if self.flags.show_help || self.flags.show_short_help {
25 | Mode::Help
26 | } else if self.flags.show_version {
27 | Mode::Version
28 | } else if self.flags.debug {
29 | Mode::Debug
30 | } else {
31 | Mode::Run
32 | }
33 | }
34 | }
35 |
36 | impl super::WorkspaceData for Workspace {
37 | fn is_json(&self) -> bool {
38 | self.flags.as_json || (!self.flags.as_form && self.has_items())
39 | }
40 | fn is_form(&self) -> bool {
41 | self.flags.as_form
42 | }
43 | fn has_items(&self) -> bool {
44 | self.items.borrow().len() > 0
45 | }
46 | }
47 |
48 | // UNIT TESTS /////////////////////////////////////////////////////////////////////////////
49 |
50 | #[cfg(test)]
51 | mod tests {
52 | mod workspace {
53 | use crate::{
54 | core::{Flags, HeaderMap, Mode, PushDataItem, Workspace, WorkspaceData},
55 | items::Items,
56 | request::Method,
57 | theme::default::DefaultTheme,
58 | };
59 | use std::cell::RefCell;
60 |
61 | #[test]
62 | fn json_flag() {
63 | let args = Workspace {
64 | method: Method::GET,
65 | urls: Vec::new(),
66 | output_redirected: false,
67 | terminal_columns: 100,
68 | theme: Box::new(DefaultTheme {}),
69 | flags: Flags {
70 | as_json: true,
71 | ..Flags::default()
72 | },
73 | headers: RefCell::new(HeaderMap::new()),
74 | items: RefCell::new(Items::new()),
75 | raw: None,
76 | certificate_authority_file: None,
77 | };
78 | assert_eq!(args.is_json(), true);
79 | assert_eq!(args.has_items(), false);
80 | assert_eq!(args.is_form(), false);
81 | assert_eq!(args.mode(), Mode::Run);
82 | }
83 |
84 | #[test]
85 | fn json_items() {
86 | let mut items = Items::new();
87 | let _ = items.push("key=value");
88 | let args = Workspace {
89 | method: Method::GET,
90 | urls: Vec::new(),
91 | output_redirected: false,
92 | terminal_columns: 100,
93 | theme: Box::new(DefaultTheme {}),
94 | flags: Flags {
95 | as_json: false,
96 | ..Flags::default()
97 | },
98 | headers: RefCell::new(HeaderMap::new()),
99 | items: RefCell::new(items),
100 | raw: None,
101 | certificate_authority_file: None,
102 | };
103 | assert_eq!(args.is_json(), true);
104 | assert_eq!(args.has_items(), true);
105 | assert_eq!(args.is_form(), false);
106 | assert_eq!(args.mode(), Mode::Run);
107 | }
108 |
109 | #[test]
110 | fn form_flag() {
111 | let args = Workspace {
112 | method: Method::GET,
113 | urls: Vec::new(),
114 | output_redirected: false,
115 | terminal_columns: 100,
116 | theme: Box::new(DefaultTheme {}),
117 | flags: Flags {
118 | as_form: true,
119 | ..Flags::default()
120 | },
121 | headers: RefCell::new(HeaderMap::new()),
122 | items: RefCell::new(Items::new()),
123 | raw: None,
124 | certificate_authority_file: None,
125 | };
126 | assert_eq!(args.is_json(), false);
127 | assert_eq!(args.has_items(), false);
128 | assert_eq!(args.is_form(), true);
129 | assert_eq!(args.mode(), Mode::Run);
130 | }
131 |
132 | #[test]
133 | fn version() {
134 | let args = Workspace {
135 | method: Method::GET,
136 | urls: Vec::new(),
137 | output_redirected: false,
138 | terminal_columns: 100,
139 | theme: Box::new(DefaultTheme {}),
140 | flags: Flags {
141 | show_version: true,
142 | ..Flags::default()
143 | },
144 | headers: RefCell::new(HeaderMap::new()),
145 | items: RefCell::new(Items::new()),
146 | raw: None,
147 | certificate_authority_file: None,
148 | };
149 | assert_eq!(args.mode(), Mode::Version);
150 | }
151 |
152 | #[test]
153 | fn help() {
154 | let args = Workspace {
155 | method: Method::GET,
156 | urls: Vec::new(),
157 | output_redirected: false,
158 | terminal_columns: 100,
159 | theme: Box::new(DefaultTheme {}),
160 | flags: Flags {
161 | show_help: true,
162 | ..Flags::default()
163 | },
164 | headers: RefCell::new(HeaderMap::new()),
165 | items: RefCell::new(Items::new()),
166 | raw: None,
167 | certificate_authority_file: None,
168 | };
169 | assert_eq!(args.mode(), Mode::Help);
170 | }
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/crates/cli/src/items/mod.rs:
--------------------------------------------------------------------------------
1 | mod number;
2 | mod ser;
3 | mod value;
4 |
5 | use crate::core::{Error, PushDataItem};
6 | use std::collections::HashMap;
7 | use value::Value;
8 |
9 | pub type Items = HashMap;
10 |
11 | const FORCE_STRING: &str = "/";
12 |
13 | impl PushDataItem for Items {
14 | fn push(&mut self, item: &str) -> Result<(), Error> {
15 | match item.split_once("=") {
16 | Some(parts) => {
17 | let key = parts.0.to_string();
18 | if key.ends_with(FORCE_STRING) {
19 | self.insert(key[..(key.len() - 1)].to_string(), Value::String(parts.1.to_string()))
20 | } else {
21 | self.insert(key, parts.1.into())
22 | }
23 | }
24 | None => return Err(Error::InvalidItem(item.into())),
25 | };
26 | Ok(())
27 | }
28 | }
29 |
30 | // UNIT TESTS /////////////////////////////////////////////////////////////////////////////
31 |
32 | #[cfg(test)]
33 | mod tests {
34 | use super::{Items, PushDataItem, Value, FORCE_STRING};
35 |
36 | macro_rules! assert_item_eq {
37 | ($item:expr, $key:expr, $value:expr) => {
38 | let mut items = Items::new();
39 | let _ = items.push($item.into());
40 |
41 | assert_eq!(items.len(), 1);
42 | assert_eq!(items.get(&$key.to_string()), Some(&$value))
43 | };
44 | }
45 |
46 | macro_rules! key_value_force_string {
47 | ($key:expr, $value:expr) => {
48 | format!("{}{}={}", $key, FORCE_STRING, $value).as_str()
49 | };
50 | }
51 |
52 | macro_rules! value_bool {
53 | ($value:expr) => {
54 | Value::Bool($value)
55 | };
56 | }
57 | macro_rules! value_string {
58 | ($value:expr) => {
59 | Value::String($value.to_string())
60 | };
61 | }
62 | macro_rules! value_number {
63 | ($value:expr) => {
64 | Value::Number($value.into())
65 | };
66 | }
67 |
68 | #[test]
69 | fn types() {
70 | assert_item_eq!("key=value", "key", value_string!("value"));
71 | assert_item_eq!("key=true", "key", value_bool!(true));
72 | assert_item_eq!("key=y", "key", value_bool!(true));
73 | assert_item_eq!("key=false", "key", value_bool!(false));
74 | assert_item_eq!("key=n", "key", value_bool!(false));
75 |
76 | assert_item_eq!("k|e|y=$true", "k|e|y", value_string!("$true"));
77 | assert_item_eq!("k.e.y=$false", "k.e.y", value_string!("$false"));
78 | assert_item_eq!(key_value_force_string!("k|e|y", "true"), "k|e|y", value_string!("true"));
79 | assert_item_eq!(key_value_force_string!("k|e|y", "y"), "k|e|y", value_string!("y"));
80 | assert_item_eq!(key_value_force_string!("k.e.y", "false"), "k.e.y", value_string!("false"));
81 | assert_item_eq!(key_value_force_string!("k|e|y", "n"), "k|e|y", value_string!("n"));
82 | assert_item_eq!(key_value_force_string!("@key", "hello"), "@key", value_string!("hello"));
83 | assert_item_eq!(key_value_force_string!("@key$", "hello"), "@key$", value_string!("hello"));
84 |
85 | assert_item_eq!("a=1", "a", value_number!(1));
86 | assert_item_eq!("bc=123", "bc", value_number!(123));
87 | assert_item_eq!("d-e=123.456", "d-e", value_number!((123.456)));
88 | assert_item_eq!("f_g=-123.456", "f_g", value_number!((-123.456)));
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/crates/cli/src/items/number.rs:
--------------------------------------------------------------------------------
1 | use std::cmp::Ordering;
2 | use std::fmt::{self, Debug, Display};
3 | use std::i64;
4 |
5 | use serde::{Serialize, Serializer};
6 |
7 | #[derive(Clone, PartialEq, PartialOrd)]
8 | pub struct Number {
9 | n: N,
10 | }
11 |
12 | impl fmt::Display for Number {
13 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
14 | match self.n {
15 | N::PosInt(i) => Display::fmt(&i, formatter),
16 | N::NegInt(i) => Display::fmt(&i, formatter),
17 | N::Float(f) if f.is_nan() => formatter.write_str(".nan"),
18 | N::Float(f) if f.is_infinite() => {
19 | if f.is_sign_negative() {
20 | formatter.write_str("-.inf")
21 | } else {
22 | formatter.write_str(".inf")
23 | }
24 | }
25 | N::Float(f) => Display::fmt(&f, formatter),
26 | }
27 | }
28 | }
29 |
30 | impl Debug for Number {
31 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
32 | Debug::fmt(&self.n, formatter)
33 | }
34 | }
35 |
36 | #[derive(Copy, Clone, Debug)]
37 | enum N {
38 | PosInt(u64),
39 | NegInt(i64),
40 | Float(f64),
41 | }
42 |
43 | impl Number {
44 | #[inline]
45 | pub fn from_str(n: &str) -> Option {
46 | match n.parse::() {
47 | Ok(num) => Some(Number { n: N::PosInt(num) }),
48 | _ => match n.parse::() {
49 | Ok(num) => Some(Number { n: N::NegInt(num) }),
50 | _ => match n.parse::() {
51 | Ok(num) => Some(Number { n: N::Float(num) }),
52 | _ => None,
53 | },
54 | },
55 | }
56 | }
57 | }
58 |
59 | impl PartialEq for N {
60 | fn eq(&self, other: &N) -> bool {
61 | match (*self, *other) {
62 | (N::PosInt(a), N::PosInt(b)) => a == b,
63 | (N::NegInt(a), N::NegInt(b)) => a == b,
64 | (N::Float(a), N::Float(b)) => {
65 | if a.is_nan() && b.is_nan() {
66 | true
67 | } else {
68 | a == b
69 | }
70 | }
71 | _ => false,
72 | }
73 | }
74 | }
75 |
76 | impl PartialOrd for N {
77 | fn partial_cmp(&self, other: &Self) -> Option {
78 | match (*self, *other) {
79 | (N::Float(a), N::Float(b)) => {
80 | if a.is_nan() && b.is_nan() {
81 | Some(Ordering::Equal)
82 | } else {
83 | a.partial_cmp(&b)
84 | }
85 | }
86 | _ => Some(self.total_cmp(other)),
87 | }
88 | }
89 | }
90 |
91 | impl N {
92 | fn total_cmp(&self, other: &Self) -> Ordering {
93 | match (*self, *other) {
94 | (N::PosInt(a), N::PosInt(b)) => a.cmp(&b),
95 | (N::NegInt(a), N::NegInt(b)) => a.cmp(&b),
96 | (N::NegInt(_), N::PosInt(_)) => Ordering::Less,
97 | (N::PosInt(_), N::NegInt(_)) => Ordering::Greater,
98 | (N::Float(a), N::Float(b)) => a.partial_cmp(&b).unwrap_or_else(|| {
99 | if !a.is_nan() {
100 | Ordering::Less
101 | } else if !b.is_nan() {
102 | Ordering::Greater
103 | } else {
104 | Ordering::Equal
105 | }
106 | }),
107 | (_, N::Float(_)) => Ordering::Less,
108 | (N::Float(_), _) => Ordering::Greater,
109 | }
110 | }
111 | }
112 |
113 | impl From for Number {
114 | #[inline]
115 | fn from(f: f64) -> Self {
116 | let n = { N::Float(f) };
117 | Number { n }
118 | }
119 | }
120 |
121 | impl Serialize for Number {
122 | #[inline]
123 | fn serialize
(&self, serializer: S) -> Result
124 | where
125 | S: Serializer,
126 | {
127 | match self.n {
128 | N::PosInt(u) => serializer.serialize_u64(u),
129 | N::NegInt(i) => serializer.serialize_i64(i),
130 | N::Float(f) => serializer.serialize_f64(f),
131 | }
132 | }
133 | }
134 |
135 | macro_rules! impl_from_unsigned {
136 | (
137 | $($ty:ty),*
138 | ) => {
139 | $(
140 | impl From<$ty> for Number {
141 | #[inline]
142 | fn from(u: $ty) -> Self {
143 | let n = {
144 | N::PosInt(u as u64)
145 | };
146 | Number { n }
147 | }
148 | }
149 | )*
150 | };
151 | }
152 |
153 | macro_rules! impl_from_signed {
154 | (
155 | $($ty:ty),*
156 | ) => {
157 | $(
158 | impl From<$ty> for Number {
159 | #[inline]
160 | fn from(i: $ty) -> Self {
161 | let n = {
162 | if i < 0 {
163 | N::NegInt(i as i64)
164 | } else {
165 | N::PosInt(i as u64)
166 | }
167 | };
168 | Number { n }
169 | }
170 | }
171 | )*
172 | };
173 | }
174 |
175 | impl_from_unsigned!(u8, u16, u32, u64, usize);
176 | impl_from_signed!(i8, i16, i32, i64, isize);
177 |
178 | // UNIT TESTS /////////////////////////////////////////////////////////////////////////////
179 |
180 | #[cfg(test)]
181 | mod tests {
182 | use super::Number;
183 |
184 | #[test]
185 | fn detect_numbers_from_str() {
186 | assert_eq!(Number::from_str("123"), Some(123u64.into()));
187 | assert_eq!(Number::from_str("18446744073709551615"), Some(18446744073709551615u64.into()));
188 | assert_eq!(Number::from_str("18446744073709551615118446744073709551615"), Some(1.8446744073709552e40f64.into()));
189 |
190 | assert_eq!(Number::from_str("-123"), Some((-123).into()));
191 | assert_eq!(Number::from_str("-9223372036854775807"), Some((-9223372036854775807i64).into()));
192 |
193 | assert_eq!(Number::from_str("123.456"), Some((123.456f64).into()));
194 | assert_eq!(Number::from_str("18446744073709551615.456"), Some((18446744073709551615.456f64).into()));
195 | assert_eq!(Number::from_str("123e10"), Some((1230000000000.0f64).into()));
196 |
197 | assert_eq!(Number::from_str("a123"), None);
198 | assert_eq!(Number::from_str("123.a"), None);
199 | assert_eq!(Number::from_str("123e"), None);
200 | assert_eq!(Number::from_str("hello"), None);
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/crates/cli/src/items/ser.rs:
--------------------------------------------------------------------------------
1 | use super::value::Value;
2 | use serde::ser::Serialize;
3 |
4 | impl Serialize for Value {
5 | #[inline]
6 | fn serialize(&self, serializer: S) -> Result
7 | where
8 | S: ::serde::Serializer,
9 | {
10 | match *self {
11 | Value::Null => serializer.serialize_unit(),
12 | Value::Bool(b) => serializer.serialize_bool(b),
13 | Value::Number(ref n) => n.serialize(serializer),
14 | Value::String(ref s) => serializer.serialize_str(s),
15 | // Value::Array(ref v) => v.serialize(serializer),
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/crates/cli/src/items/value.rs:
--------------------------------------------------------------------------------
1 | use super::number::Number;
2 |
3 | #[derive(Clone, PartialEq, Debug)]
4 | pub enum Value {
5 | Null,
6 | Bool(bool),
7 | Number(Number),
8 | String(String),
9 | // Array(Vec),
10 | }
11 |
12 | impl<'a> From<&'a str> for Value {
13 | fn from(value: &str) -> Self {
14 | match value {
15 | "true" | "y" => Value::Bool(true),
16 | "false" | "n" => Value::Bool(false),
17 | "" => Value::Null,
18 | _ => match Number::from_str(value) {
19 | Some(num) => Value::Number(num),
20 | _ => Value::String(value.to_string()),
21 | },
22 | }
23 | }
24 | }
25 |
26 | // UNIT TESTS /////////////////////////////////////////////////////////////////////////////
27 |
28 | #[cfg(test)]
29 | mod tests {
30 | use super::Value;
31 |
32 | macro_rules! assert_value_eq {
33 | ($value:expr, $expected:expr) => {
34 | let value: Value = $value.into();
35 | assert_eq!(value, $expected)
36 | };
37 | }
38 |
39 | macro_rules! value_string {
40 | ($value:expr) => {
41 | Value::String($value.to_string())
42 | };
43 | }
44 | macro_rules! value_number {
45 | ($value:expr) => {
46 | Value::Number($value.into())
47 | };
48 | }
49 |
50 | #[test]
51 | fn detect_type_from_str() {
52 | assert_value_eq!("true", Value::Bool(true));
53 | assert_value_eq!("y", Value::Bool(true));
54 | assert_value_eq!("false", Value::Bool(false));
55 | assert_value_eq!("n", Value::Bool(false));
56 |
57 | assert_value_eq!("$", value_string!("$"));
58 | assert_value_eq!("hello", value_string!("hello"));
59 |
60 | assert_value_eq!("1", value_number!(1));
61 | assert_value_eq!("123", value_number!(123));
62 | assert_value_eq!("123.456", value_number!((123.456)));
63 | assert_value_eq!("-123.456", value_number!((-123.456)));
64 |
65 | assert_value_eq!("", Value::Null);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/crates/cli/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod app;
2 | mod commands;
3 | mod core;
4 | mod items;
5 | mod macros;
6 | mod parser;
7 | mod request;
8 | pub mod shell;
9 | #[cfg(test)]
10 | pub mod test;
11 | mod theme;
12 |
13 | use crate::app::App;
14 | use crate::shell::os::OsDirs;
15 | use crate::shell::Shell;
16 | use std::io::Write;
17 |
18 | #[inline]
19 | pub fn run<'a, OD: OsDirs, O: Write, E: Write>(args: &mut Vec, shell: &'a mut Shell<'a, OD, O, E>) -> i32 {
20 | let mut app = App::new(shell);
21 | match app.run(args) {
22 | Ok(_) => app.exit_code(None),
23 | Err(err) => app.exit_code(Some(err)),
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/crates/cli/src/macros.rs:
--------------------------------------------------------------------------------
1 | #[macro_export]
2 | macro_rules! ifelse {
3 | ($condition: expr, $_true: expr, $_false: expr) => {
4 | if $condition {
5 | $_true
6 | } else {
7 | $_false
8 | }
9 | };
10 | }
11 |
12 | #[macro_export]
13 | macro_rules! rh_name {
14 | () => {
15 | env!("CARGO_PKG_NAME")
16 | };
17 | }
18 |
19 | #[macro_export]
20 | macro_rules! rh_version {
21 | () => {
22 | env!("CARGO_PKG_VERSION")
23 | };
24 | }
25 |
26 | #[macro_export]
27 | macro_rules! rh_homepage {
28 | () => {
29 | env!("CARGO_PKG_HOMEPAGE")
30 | };
31 | }
32 |
33 | #[cfg(test)]
34 | mod tests {
35 | #[test]
36 | fn ifelse() {
37 | let res = ifelse![true, true, false];
38 | assert_eq!(res, true);
39 |
40 | let res = ifelse![false, true, false];
41 | assert_eq!(res, false);
42 |
43 | let val = true;
44 | let res = ifelse![val, 1, 2];
45 | assert_eq!(res, 1);
46 |
47 | let val = false;
48 | let res = ifelse![val, 1, 2];
49 | assert_eq!(res, 2);
50 |
51 | let val = 2;
52 | let res = ifelse![val == 2, "yes", "no"];
53 | assert_eq!(res, "yes");
54 |
55 | let val = 3;
56 | let res = ifelse![val == 2, "yes", "no"];
57 | assert_eq!(res, "no");
58 | }
59 |
60 | #[test]
61 | fn crate_name() {
62 | assert_eq!(rh_name!(), "rh");
63 | }
64 |
65 | #[test]
66 | fn rh_version() {
67 | assert_eq!(rh_version!(), env!("CARGO_PKG_VERSION"));
68 | }
69 |
70 | #[test]
71 | fn rh_homepage() {
72 | assert_eq!(rh_homepage!(), env!("CARGO_PKG_HOMEPAGE"));
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/crates/cli/src/main.rs:
--------------------------------------------------------------------------------
1 | use rh::shell::os::DefaultOsDirs;
2 | use rh::shell::Shell;
3 | use std::env;
4 | use std::io;
5 | use std::process::exit;
6 |
7 | fn main() {
8 | let mut os_args = env::args().skip(1).collect::>();
9 |
10 | // let stdout = io::stdout();
11 | // let out = io::BufWriter::new(stdout.lock());
12 | // let stderr = io::stderr();
13 | // let err = io::BufWriter::new(stderr.lock());
14 | // let mut shell = Shell::new(out, err);
15 |
16 | let out = io::stdout();
17 | let err = io::stderr();
18 | let os_dirs = DefaultOsDirs::default();
19 | let mut shell = Shell::new(&os_dirs, out, err);
20 |
21 | let exit_code = rh::run(&mut os_args, &mut shell);
22 | exit(exit_code);
23 | }
24 |
--------------------------------------------------------------------------------
/crates/cli/src/parser/core.rs:
--------------------------------------------------------------------------------
1 | use regex::Regex;
2 |
3 | pub const RAW_FLAG: &str = "--raw=";
4 | pub const CAFILE_FLAG: &str = "--cafile=";
5 |
6 | pub trait ArgDetection {
7 | fn is_raw_flag(&self) -> bool;
8 | fn is_cafile_flag(&self) -> bool;
9 | fn is_flag(&self) -> bool;
10 | fn is_header(&self) -> bool;
11 | fn is_item(&self) -> bool;
12 |
13 | fn is_likely_url(&self) -> bool;
14 | fn is_very_likely_url(&self) -> bool;
15 | fn is_url(&self) -> bool;
16 | }
17 |
18 | impl ArgDetection for String {
19 | fn is_raw_flag(&self) -> bool {
20 | self.starts_with(RAW_FLAG)
21 | }
22 | fn is_cafile_flag(&self) -> bool {
23 | self.starts_with(CAFILE_FLAG)
24 | }
25 | fn is_flag(&self) -> bool {
26 | self.starts_with('-')
27 | }
28 | fn is_header(&self) -> bool {
29 | match self.chars().next() {
30 | Some(first_char) => first_char.is_ascii_alphanumeric() && self.contains(':') && !self.contains('='),
31 | None => false,
32 | }
33 | }
34 | fn is_item(&self) -> bool {
35 | !self.starts_with('=') && !self.starts_with('/') && !self.starts_with(':') && self.contains('=')
36 | }
37 |
38 | fn is_likely_url(&self) -> bool {
39 | !self.is_flag()
40 | }
41 | fn is_very_likely_url(&self) -> bool {
42 | Regex::new(r"^\w+[-\.\w]*:+\d{1,5}$|^\w+[-\.\w]*$").unwrap().is_match(self)
43 | }
44 | fn is_url(&self) -> bool {
45 | // FIXME Add IPv6 and IPv4 detection
46 | self.starts_with("http://") || self.starts_with("https://") || self.starts_with(':') || self.starts_with('/')
47 | }
48 | }
49 |
50 | // UNIT TESTS /////////////////////////////////////////////////////////////////////////////
51 |
52 | #[cfg(test)]
53 | mod tests {
54 | use super::ArgDetection;
55 |
56 | macro_rules! arg {
57 | ($val:expr) => {
58 | String::from($val)
59 | };
60 | }
61 |
62 | #[test]
63 | fn raw_flag() {
64 | assert!(arg!("--raw=").is_raw_flag());
65 | assert!(arg!("--raw=data").is_raw_flag());
66 | }
67 | #[test]
68 | fn not_raw_flag() {
69 | assert!(!arg!("--raw").is_raw_flag());
70 | assert!(!arg!("-raw").is_raw_flag());
71 | assert!(!arg!("-raw=").is_raw_flag());
72 | assert!(!arg!("-raw=data").is_raw_flag());
73 | }
74 |
75 | #[test]
76 | fn ca_flag() {
77 | assert!(arg!("--cafile=").is_cafile_flag());
78 | assert!(arg!("--cafile=path").is_cafile_flag());
79 | }
80 | #[test]
81 | fn not_ca_flag() {
82 | assert!(!arg!("--cafile").is_cafile_flag());
83 | assert!(!arg!("-cafile").is_cafile_flag());
84 | assert!(!arg!("-cafile=").is_cafile_flag());
85 | assert!(!arg!("-cafile=data").is_cafile_flag());
86 | }
87 |
88 | #[test]
89 | fn flag() {
90 | assert!(arg!("-").is_flag());
91 | assert!(arg!("-raw").is_flag());
92 | assert!(arg!("--raw=").is_flag());
93 | assert!(arg!("--raw=data").is_flag());
94 | assert!(arg!("-aBc").is_flag());
95 | }
96 | #[test]
97 | fn not_flag() {
98 | assert!(!arg!("not-a-flag-").is_flag());
99 | }
100 |
101 | #[test]
102 | fn header() {
103 | assert!(arg!("Key:Value").is_header());
104 | assert!(arg!("Key-1:Value/hello/bye").is_header());
105 | }
106 | #[test]
107 | fn not_header() {
108 | assert!(!arg!(".Key:Value").is_header());
109 | assert!(!arg!(":Key:Value").is_header());
110 | assert!(!arg!("/Key:Value").is_header());
111 | assert!(!arg!("Key:SubKey=Value").is_header());
112 | }
113 |
114 | #[test]
115 | fn item() {
116 | assert!(arg!("Key=Value").is_item());
117 | assert!(arg!(".Key=.Value").is_item());
118 | assert!(arg!("Key=Value:SubValue").is_item());
119 | }
120 | #[test]
121 | fn not_item() {
122 | assert!(!arg!(":Key=Value").is_item());
123 | assert!(!arg!("/Key=Value").is_item());
124 | assert!(!arg!("=Key=Value").is_item());
125 | }
126 |
127 | #[test]
128 | fn likely_url() {
129 | assert!(arg!("anything").is_likely_url());
130 | }
131 | #[test]
132 | fn not_likely_url() {
133 | assert_eq!(arg!("--a-flag-is-not-likely-an-url").is_likely_url(), false);
134 | }
135 |
136 | #[test]
137 | fn very_likely_url() {
138 | assert!(arg!("localhost").is_very_likely_url());
139 | assert!(arg!("localhost:1").is_very_likely_url());
140 | assert!(arg!("localhost:12").is_very_likely_url());
141 | assert!(arg!("localhost:123").is_very_likely_url());
142 | assert!(arg!("localhost:1234").is_very_likely_url());
143 | assert!(arg!("localhost:12345").is_very_likely_url());
144 | assert!(arg!("anything").is_very_likely_url());
145 | assert!(arg!("anything:8080").is_very_likely_url());
146 | assert!(arg!("my-hostname").is_very_likely_url());
147 | assert!(arg!("my-hostname:12345").is_very_likely_url());
148 | assert!(arg!("test.com").is_very_likely_url());
149 | assert!(arg!("test.com:80").is_very_likely_url());
150 | assert!(arg!("test.co.uk").is_very_likely_url());
151 | assert!(arg!("test.co.uk:443").is_very_likely_url());
152 | assert!(arg!("a.uk").is_very_likely_url());
153 | assert!(arg!("b.uk:1").is_very_likely_url());
154 | }
155 |
156 | #[test]
157 | fn not_very_likely_url() {
158 | assert_eq!(arg!("anything:hi").is_very_likely_url(), false);
159 | assert_eq!(arg!("anything:808055").is_very_likely_url(), false);
160 | assert_eq!(arg!("my-hostname:hello").is_very_likely_url(), false);
161 | assert_eq!(arg!("my-hostname:123456").is_very_likely_url(), false);
162 | assert_eq!(arg!(":test.com").is_very_likely_url(), false);
163 | assert_eq!(arg!("test.com:abcdef").is_very_likely_url(), false);
164 | assert_eq!(arg!("test.com:654321").is_very_likely_url(), false);
165 | assert_eq!(arg!("test.co.uk:qwerty").is_very_likely_url(), false);
166 | assert_eq!(arg!("-test.co.uk").is_very_likely_url(), false);
167 | assert_eq!(arg!(".test.co.uk").is_very_likely_url(), false);
168 | assert_eq!(arg!("@test.co.uk").is_very_likely_url(), false);
169 | assert_eq!(arg!("/test.co.uk").is_very_likely_url(), false);
170 | assert_eq!(arg!("*test.co.uk").is_very_likely_url(), false);
171 | }
172 |
173 | #[test]
174 | fn url() {
175 | assert!(arg!("http://test.com").is_url());
176 | assert!(arg!("https://test.com").is_url());
177 | assert!(arg!("/path/hello?r=y").is_url());
178 | assert!(arg!("/").is_url());
179 | assert!(arg!("/path").is_url());
180 | assert!(arg!(":").is_url());
181 | assert!(arg!(":9200").is_url());
182 | }
183 | #[test]
184 | fn not_url() {
185 | assert_eq!(arg!("not-anything").is_url(), false);
186 | assert_eq!(arg!("--a-flag-is-not-an-url").is_url(), false);
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/crates/cli/src/parser/error.rs:
--------------------------------------------------------------------------------
1 | use crate::core::Error;
2 | use std::io;
3 |
4 | impl From for Error {
5 | fn from(err: io::Error) -> Error {
6 | Error::Io(err.to_string())
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/crates/cli/src/parser/flags.rs:
--------------------------------------------------------------------------------
1 | use super::Error;
2 | use crate::core::Flags;
3 | use regex::Regex;
4 |
5 | impl Default for Flags {
6 | fn default() -> Self {
7 | Self {
8 | show_version: false,
9 | show_help: false,
10 | show_short_help: false,
11 | debug: false,
12 |
13 | https: false,
14 | http: false,
15 | use_color: true,
16 | show_direction: false,
17 |
18 | as_json: false,
19 | as_form: false,
20 |
21 | show_request_url: false,
22 | show_request_headers: false,
23 | show_request_compact: false,
24 | show_request_body: false,
25 |
26 | show_response_status: false,
27 | show_response_headers: false,
28 | show_response_compact: false,
29 | show_response_body: true,
30 | }
31 | }
32 | }
33 |
34 | impl Flags {
35 | pub fn new(output_redirected: bool) -> Flags {
36 | Flags {
37 | use_color: !output_redirected,
38 | ..Default::default()
39 | }
40 | }
41 |
42 | pub fn push(&mut self, flag: &str) -> Result<(), Error> {
43 | match flag {
44 | "--version" => self.show_version = true,
45 | "--help" => self.show_help = true,
46 | "--debug" => self.debug = true,
47 | "-U" | "--url" => self.show_request_url = true,
48 | "-s" | "--status" => self.show_response_status = true,
49 | "-d" | "--direction" => self.show_direction = true,
50 | "-v" | "--verbose" => self.enable_verbose(),
51 | "--pretty=c" | "--pretty=color" => self.use_color = true,
52 | "--json" => self.as_json = true,
53 | "--form" => self.as_form = true,
54 | "--http" => {
55 | self.http = true;
56 | if self.is_contradictory_scheme() {
57 | return Err(Error::ContradictoryScheme);
58 | }
59 | }
60 | "--https" | "--ssl" => {
61 | self.https = true;
62 | if self.is_contradictory_scheme() {
63 | return Err(Error::ContradictoryScheme);
64 | }
65 | }
66 | "--headers" => {
67 | self.show_request_headers = true;
68 | self.show_response_headers = true;
69 | }
70 | "-H" | "--req-headers" => self.show_request_headers = true,
71 | "-h" => {
72 | self.show_short_help = true;
73 | self.show_response_headers = true;
74 | }
75 | "--header" => self.show_response_headers = true,
76 | "-B" | "--req-body" => self.show_request_body = true,
77 | "-b" | "--body" => self.show_response_body = true,
78 | "-C" | "--req-compact" => self.show_request_compact = true,
79 | "-c" | "--compact" => self.show_response_compact = true,
80 | _ => {
81 | let has_valid_compact_flags = self.extract_compact_flags(flag);
82 | if !has_valid_compact_flags {
83 | return Err(Error::InvalidFlag(flag.to_string()));
84 | }
85 | }
86 | };
87 | Ok(())
88 | }
89 |
90 | fn extract_compact_flags(&mut self, flag: &str) -> bool {
91 | // FIXME Need something like "-no-bBH..." to set the related flags to false
92 | let valid = Regex::new(r"^\-[vcCdUshHbB]*$").unwrap().is_match(flag);
93 | if valid {
94 | if flag.contains('v') {
95 | self.enable_verbose();
96 | }
97 | if flag.contains('c') {
98 | self.show_response_compact = true;
99 | }
100 | if flag.contains('C') {
101 | self.show_request_compact = true;
102 | }
103 | if flag.contains('d') {
104 | self.show_direction = true;
105 | }
106 | if flag.contains('U') {
107 | self.show_request_url = true;
108 | }
109 | if flag.contains('s') {
110 | self.show_response_status = true;
111 | }
112 | if flag.contains('H') {
113 | self.show_request_headers = true;
114 | }
115 | if flag.contains('h') {
116 | self.show_response_headers = true;
117 | }
118 | if flag.contains('b') {
119 | self.show_response_body = true;
120 | }
121 | if flag.contains('B') {
122 | self.show_request_body = true;
123 | }
124 | }
125 | valid
126 | }
127 |
128 | fn enable_verbose(&mut self) {
129 | self.show_direction = true;
130 | self.show_request_url = true;
131 | self.show_response_status = true;
132 | self.show_request_headers = true;
133 | self.show_response_headers = true;
134 | self.show_request_body = true;
135 | self.show_response_body = true;
136 | }
137 |
138 | fn is_contradictory_scheme(&self) -> bool {
139 | self.http && self.https
140 | }
141 | }
142 |
143 | // UNIT TESTS /////////////////////////////////////////////////////////////////////////////
144 |
145 | #[cfg(test)]
146 | mod tests {
147 | use super::{Error, Flags};
148 |
149 | macro_rules! flag {
150 | () => {{
151 | Flags::new(false)
152 | }};
153 | ( $( $elem:expr ),* ) => {
154 | {
155 | let mut temp_flags = Flags::new(false);
156 | $(
157 | let _ = temp_flags.push($elem);
158 | )*
159 | temp_flags
160 | }
161 | };
162 | }
163 |
164 | #[test]
165 | fn valid_scheme() {
166 | let flags = flag!["--http"];
167 | assert_eq!(flags.http, true);
168 |
169 | let flags = flag!["--https"];
170 | assert_eq!(flags.https, true);
171 | }
172 |
173 | #[test]
174 | fn contradictory_scheme() {
175 | let mut flags = flag![];
176 | let _ = flags.push("--http");
177 | let res = flags.push("--https");
178 | assert!(res.is_err());
179 | assert_eq!(res.unwrap_err(), Error::ContradictoryScheme);
180 |
181 | let mut flags = flag![];
182 | let _ = flags.push("--https");
183 | let res = flags.push("--http");
184 | assert!(res.is_err());
185 | assert_eq!(res.unwrap_err(), Error::ContradictoryScheme);
186 |
187 | let mut flags = flag!["-H", "-h"];
188 | let _ = flags.push("--https");
189 | let res = flags.push("--http");
190 | assert!(res.is_err());
191 | assert_eq!(res.unwrap_err(), Error::ContradictoryScheme);
192 | }
193 |
194 | #[test]
195 | fn compact_flags() {
196 | let flags = flag!["-hH"];
197 | assert_eq!(flags.show_request_headers, true);
198 | assert_eq!(flags.show_response_headers, true);
199 |
200 | let flags = flag!["-Hh"];
201 | assert_eq!(flags.show_request_headers, true);
202 | assert_eq!(flags.show_response_headers, true);
203 |
204 | let flag = "-hHa";
205 | let mut flags = flag![];
206 | let res = flags.push(flag);
207 | assert!(res.is_err());
208 | assert_eq!(res.unwrap_err(), Error::InvalidFlag(flag.into()));
209 |
210 | let flag = "-ahH";
211 | let mut flags = flag![];
212 | let res = flags.push(flag);
213 | assert!(res.is_err());
214 | assert_eq!(res.unwrap_err(), Error::InvalidFlag(flag.into()));
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/crates/cli/src/parser/headers.rs:
--------------------------------------------------------------------------------
1 | pub use crate::core::HeaderMap;
2 | use crate::core::{Error, PushDataItem, Result};
3 | use reqwest::header::{HeaderName, HeaderValue};
4 | use std::str::FromStr;
5 |
6 | impl PushDataItem for HeaderMap {
7 | fn push(&mut self, item: &str) -> Result<()> {
8 | match item.split_once(":") {
9 | Some(parts) => {
10 | let key = HeaderName::from_str(parts.0)?;
11 | let value = HeaderValue::from_str(parts.1)?;
12 | self.append(key, value);
13 | }
14 | None => return Err(Error::InvalidHeader(item.into())),
15 | };
16 | Ok(())
17 | }
18 | }
19 |
20 | impl From for Error {
21 | fn from(err: reqwest::header::InvalidHeaderName) -> Error {
22 | Error::BadHeaderName(err.to_string()) // FIXME The err doesn't contain the header name
23 | }
24 | }
25 |
26 | impl From for Error {
27 | fn from(err: reqwest::header::InvalidHeaderValue) -> Error {
28 | Error::BadHeaderValue(err.to_string()) // FIXME The err doesn't contain the header value
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/crates/cli/src/parser/method.rs:
--------------------------------------------------------------------------------
1 | use crate::request::Method;
2 |
3 | pub fn from_str(keyword: &str) -> Option {
4 | if !is_valid(keyword) {
5 | return None;
6 | }
7 |
8 | let method = reqwest::Method::from_bytes(keyword.as_bytes());
9 | match method {
10 | Ok(m) => Some(m),
11 | _ => None,
12 | }
13 | }
14 |
15 | fn is_valid(keyword: &str) -> bool {
16 | for c in keyword.chars() {
17 | if !c.is_uppercase() {
18 | return false;
19 | }
20 | }
21 | true
22 | }
23 |
24 | // fn is_standard(keyword: &str) -> bool {
25 | // let length = keyword.len();
26 | // if length != 3 && length != 4 && length != 5 && length != 6 && length != 7 {
27 | // return false;
28 | // }
29 | // [
30 | // "GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "CONNECT", "PATCH", "TRACE",
31 | // ]
32 | // .contains(&keyword)
33 | // }
34 |
35 | // UNIT TESTS /////////////////////////////////////////////////////////////////////////////
36 |
37 | #[cfg(test)]
38 | mod tests {
39 | use super::{from_str, Method};
40 |
41 | macro_rules! assert_standard_method_eq {
42 | ($method:expr, $expected:expr) => {
43 | assert_eq!(from_str($method).unwrap(), $expected)
44 | };
45 | }
46 |
47 | macro_rules! assert_standard_method_invalid {
48 | ($method:expr) => {
49 | assert!(from_str($method).is_none())
50 | };
51 | }
52 |
53 | #[test]
54 | fn standard() {
55 | assert_standard_method_eq!("GET", Method::GET);
56 | assert_standard_method_eq!("POST", Method::POST);
57 | assert_standard_method_eq!("PUT", Method::PUT);
58 | assert_standard_method_eq!("DELETE", Method::DELETE);
59 | assert_standard_method_eq!("HEAD", Method::HEAD);
60 | assert_standard_method_eq!("OPTIONS", Method::OPTIONS);
61 | assert_standard_method_eq!("CONNECT", Method::CONNECT);
62 | assert_standard_method_eq!("PATCH", Method::PATCH);
63 | assert_standard_method_eq!("TRACE", Method::TRACE);
64 | }
65 |
66 | #[test]
67 | fn custom() {
68 | assert_standard_method_eq!("HELLO", Method::from_bytes(b"HELLO").unwrap());
69 | assert_standard_method_eq!("WORLD", Method::from_bytes(b"WORLD").unwrap());
70 | }
71 |
72 | #[test]
73 | fn invalid() {
74 | assert_standard_method_invalid!("test");
75 |
76 | assert_standard_method_invalid!("get");
77 | assert_standard_method_invalid!("post");
78 | assert_standard_method_invalid!("put");
79 | assert_standard_method_invalid!("delete");
80 | assert_standard_method_invalid!("head");
81 | assert_standard_method_invalid!("options");
82 | assert_standard_method_invalid!("connect");
83 | assert_standard_method_invalid!("patch");
84 | assert_standard_method_invalid!("trace");
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/crates/cli/src/parser/mod.rs:
--------------------------------------------------------------------------------
1 | mod core;
2 | mod error;
3 | mod flags;
4 | mod headers;
5 | mod method;
6 | mod normalizer;
7 | mod url;
8 |
9 | use crate::core::{Error, Flags, Result, Workspace};
10 | use crate::items::Items;
11 | use crate::shell::stream;
12 | use crate::theme::default::DefaultTheme;
13 | use normalizer::Normalizer;
14 | use std::cell::RefCell;
15 | use std::io::{self, Read};
16 |
17 | pub fn execute(args: &[String]) -> Result {
18 | validate_there_are_enough_args(args)?;
19 |
20 | let output_redirected = !stream::is_stdout();
21 | let mut normalizer = Normalizer::parse(args, output_redirected, "http", "localhost")?;
22 | let method = normalizer.method();
23 | let flags = normalizer.flags;
24 | let headers = normalizer.headers;
25 | let items = normalizer.items;
26 | let urls = normalizer.urls;
27 | let mut raw = normalizer.raw.take();
28 | let certificate_authority_file = normalizer.certificate_authority_file.take();
29 |
30 | let input_redirected = !stream::is_stdin();
31 | if !is_flag_only_command(&flags) {
32 | validate_processed_urls(&urls, &flags, args)?;
33 | validate_there_is_no_mix_of_items_and_raw_and_stdin(&items, &raw, input_redirected)?;
34 | }
35 |
36 | if input_redirected {
37 | extract_input_as_raw_data(&mut raw)?;
38 | }
39 |
40 | Ok(Workspace {
41 | method,
42 | urls,
43 | output_redirected,
44 | terminal_columns: terminal_columns(),
45 | theme: Box::new(DefaultTheme::new()),
46 | flags,
47 | headers: RefCell::new(headers),
48 | items: RefCell::new(items),
49 | raw,
50 | certificate_authority_file,
51 | })
52 | }
53 |
54 | fn extract_input_as_raw_data(raw: &mut Option) -> Result<()> {
55 | let mut buffer = String::new();
56 | io::stdin().read_to_string(&mut buffer)?;
57 | *raw = Some(buffer);
58 | Ok(())
59 | }
60 |
61 | #[inline]
62 | fn validate_there_are_enough_args(args: &[String]) -> Result<()> {
63 | let count = args.len();
64 | if count == 0 {
65 | Err(Error::NoArgs)
66 | } else {
67 | Ok(())
68 | }
69 | }
70 |
71 | #[inline]
72 | fn validate_processed_urls(urls: &[String], flags: &Flags, args: &[String]) -> Result<()> {
73 | if urls.is_empty() {
74 | if short_help_flag(flags, args) {
75 | Ok(())
76 | } else {
77 | Err(Error::MissingUrl)
78 | }
79 | } else {
80 | Ok(())
81 | }
82 | }
83 |
84 | #[inline]
85 | fn validate_there_is_no_mix_of_items_and_raw_and_stdin(items: &Items, raw: &Option, input_redirected: bool) -> Result<()> {
86 | if (!items.is_empty()) as u8 + raw.is_some() as u8 + input_redirected as u8 > 1 {
87 | Err(Error::ItemsAndRawMix)
88 | } else {
89 | Ok(())
90 | }
91 | }
92 |
93 | #[inline]
94 | fn is_flag_only_command(flags: &Flags) -> bool {
95 | flags.show_version || flags.show_help || flags.debug
96 | }
97 |
98 | #[inline]
99 | fn short_help_flag(flags: &Flags, args: &[String]) -> bool {
100 | flags.show_short_help && args.len() == 1
101 | }
102 |
103 | fn terminal_columns() -> u16 {
104 | match termsize::get() {
105 | Some(size) => size.cols,
106 | None => 100,
107 | }
108 | }
109 |
110 | // UNIT TESTS /////////////////////////////////////////////////////////////////////////////
111 |
112 | #[cfg(test)]
113 | mod tests {
114 | use super::*;
115 |
116 | mod basic {
117 | use super::*;
118 |
119 | #[test]
120 | fn show_version() {
121 | let args = rh_test::args!["--version"];
122 | let parser = execute(&args).unwrap();
123 | assert_eq!(parser.flags.show_version, true);
124 | }
125 |
126 | // #[test]
127 | // fn show_short_version() {
128 | // let args = rh_test::args!["-v"];
129 | // let parser = execute(&args).unwrap();
130 | // assert_eq!(parser.flags.show_short_version, true);
131 | // }
132 |
133 | #[test]
134 | fn show_help() {
135 | let args = rh_test::args!["--help"];
136 | let parser = execute(&args).unwrap();
137 | assert_eq!(parser.flags.show_help, true);
138 | }
139 |
140 | #[test]
141 | fn show_short_help() {
142 | let args = rh_test::args!["-h"];
143 | let parser = execute(&args).unwrap();
144 | assert_eq!(parser.flags.show_short_help, true);
145 | }
146 | }
147 |
148 | mod validate {
149 | use super::*;
150 | use crate::core::PushDataItem;
151 |
152 | const NO_STDIN_DATA: bool = false;
153 | const STDIN_DATA: bool = true;
154 |
155 | #[test]
156 | fn flag_only_commands() {
157 | let mut flags = Flags::default();
158 | flags.show_help = true;
159 | assert!(is_flag_only_command(&flags));
160 |
161 | let mut flags = Flags::default();
162 | flags.show_version = true;
163 | assert!(is_flag_only_command(&flags));
164 | }
165 |
166 | #[test]
167 | fn error_if_no_args() {
168 | let args = rh_test::args![];
169 | let parser = validate_there_are_enough_args(&args);
170 | assert!(parser.is_err());
171 | assert_eq!(parser.unwrap_err(), Error::NoArgs);
172 | }
173 |
174 | #[test]
175 | fn basic_validation_if_multi_args() {
176 | let args = rh_test::args!["GET", "localhost"];
177 | let parser = validate_there_are_enough_args(&args);
178 | assert!(parser.is_ok());
179 | }
180 |
181 | #[test]
182 | fn error_if_no_urls() {
183 | let args = rh_test::args![];
184 | let flags = Flags::default();
185 | let parser = validate_processed_urls(&[], &flags, &args);
186 | assert!(parser.is_err());
187 | assert_eq!(parser.unwrap_err(), Error::MissingUrl);
188 | }
189 |
190 | #[test]
191 | fn validate_if_one_url() {
192 | let args = rh_test::args!["test.com"];
193 | let flags = Flags::default();
194 | let urls = rh_test::args!["test.com"];
195 | let parser = validate_processed_urls(&urls, &flags, &args);
196 | assert!(parser.is_ok());
197 | }
198 |
199 | #[test]
200 | fn raw_data_only() {
201 | let items = Items::new();
202 | let raw_data = Some("hello".into());
203 | let parser = validate_there_is_no_mix_of_items_and_raw_and_stdin(&items, &raw_data, NO_STDIN_DATA);
204 | assert!(parser.is_ok());
205 | }
206 |
207 | #[test]
208 | fn key_value_only() {
209 | let mut items = Items::new();
210 | let _ = items.push("key=value");
211 | let parser = validate_there_is_no_mix_of_items_and_raw_and_stdin(&items, &None, NO_STDIN_DATA);
212 | assert!(parser.is_ok());
213 | }
214 |
215 | #[test]
216 | fn stdin_only() {
217 | let items = Items::new();
218 | let parser = validate_there_is_no_mix_of_items_and_raw_and_stdin(&items, &None, STDIN_DATA);
219 | assert!(parser.is_ok());
220 | }
221 |
222 | #[test]
223 | fn error_if_mix_raw_and_stdin() {
224 | let items = Items::new();
225 | let raw_data = Some("hello".into());
226 | let parser = validate_there_is_no_mix_of_items_and_raw_and_stdin(&items, &raw_data, STDIN_DATA);
227 | assert!(parser.is_err());
228 | assert_eq!(parser.unwrap_err(), Error::ItemsAndRawMix);
229 | }
230 |
231 | #[test]
232 | fn error_if_mix_key_value_and_raw() {
233 | let mut items = Items::new();
234 | items.push("key=value").expect("Cannot add key/value item");
235 | let raw_data = Some("hello".into());
236 | let parser = validate_there_is_no_mix_of_items_and_raw_and_stdin(&items, &raw_data, NO_STDIN_DATA);
237 | assert!(parser.is_err());
238 | assert_eq!(parser.unwrap_err(), Error::ItemsAndRawMix);
239 | }
240 |
241 | #[test]
242 | fn error_if_mix_key_value_and_stdin() {
243 | let mut items = Items::new();
244 | items.push("key=value").expect("Cannot add key/value item");
245 | let raw_data = Some("hello".into());
246 | let parser = validate_there_is_no_mix_of_items_and_raw_and_stdin(&items, &raw_data, STDIN_DATA);
247 | assert!(parser.is_err());
248 | assert_eq!(parser.unwrap_err(), Error::ItemsAndRawMix);
249 | }
250 | }
251 |
252 | mod urls {
253 | use super::*;
254 |
255 | #[test]
256 | fn hostname_only() {
257 | let args = rh_test::args!["localhost"];
258 | let parser = execute(&args).unwrap();
259 | assert_eq!(parser.urls.len(), 1);
260 | rh_test::assert_str_eq!(parser.urls[0], "http://localhost");
261 | }
262 |
263 | #[test]
264 | fn method_and_hostname() {
265 | let args = rh_test::args!["GET", "localhost"];
266 | let parser = execute(&args).unwrap();
267 | assert_eq!(parser.urls.len(), 1);
268 | rh_test::assert_str_eq!(parser.urls[0], "http://localhost");
269 | }
270 |
271 | #[test]
272 | fn method_and_hostname_and_flag() {
273 | let args = rh_test::args!["GET", "localhost", "--headers"];
274 | let parser = execute(&args).unwrap();
275 | assert_eq!(parser.urls.len(), 1);
276 | rh_test::assert_str_eq!(parser.urls[0], "http://localhost");
277 | assert_eq!(parser.flags.show_request_headers, true);
278 | assert_eq!(parser.flags.show_response_headers, true);
279 | }
280 |
281 | #[test]
282 | fn detect_obvious_url() {
283 | let args = rh_test::args!["GET", "--url", "http://test.com", "--headers"];
284 | let parser = execute(&args).unwrap();
285 | assert_eq!(parser.urls.len(), 1);
286 | rh_test::assert_str_eq!(parser.urls[0], "http://test.com");
287 | assert_eq!(parser.flags.show_request_url, true);
288 | assert_eq!(parser.flags.show_request_headers, true);
289 | assert_eq!(parser.flags.show_response_headers, true);
290 | }
291 |
292 | #[test]
293 | fn error_if_multi_args_including_method_but_method_at_wrong_place() {
294 | let args = rh_test::args!["GET", "--url", "--headers", "https://test.com"];
295 | let parser = execute(&args).unwrap();
296 | assert_eq!(parser.urls.len(), 1);
297 | rh_test::assert_str_eq!(parser.urls[0], "https://test.com");
298 | assert_eq!(parser.flags.show_request_url, true);
299 | assert_eq!(parser.flags.show_request_headers, true);
300 | assert_eq!(parser.flags.show_response_headers, true);
301 | }
302 |
303 | #[test]
304 | fn error_if_one_arg_but_no_url() {
305 | let args: Vec = rh_test::args!["--url"];
306 | let parser = execute(&args);
307 | assert!(parser.is_err());
308 | assert_eq!(parser.unwrap_err(), Error::MissingUrl);
309 | }
310 |
311 | #[test]
312 | fn error_if_multi_args_but_no_url() {
313 | let args = rh_test::args!["--url", "--headers"];
314 | let parser = execute(&args);
315 | assert!(parser.is_err());
316 | assert_eq!(parser.unwrap_err(), Error::MissingUrl);
317 | }
318 |
319 | #[test]
320 | fn error_if_multi_args_including_method_but_no_url() {
321 | let args = rh_test::args!["GET", "--url", "--headers"];
322 | let parser = execute(&args);
323 | assert!(parser.is_err());
324 | assert_eq!(parser.unwrap_err(), Error::MissingUrl);
325 | }
326 | }
327 |
328 | mod raw {
329 | use super::*;
330 |
331 | #[test]
332 | fn error_if_raw_data_and_json() {
333 | let args: Vec = rh_test::args!["test.com", "--raw=data", "key=value"];
334 | let parser = execute(&args);
335 | assert!(parser.is_err());
336 | assert_eq!(parser.unwrap_err(), Error::ItemsAndRawMix);
337 | }
338 | }
339 | }
340 |
--------------------------------------------------------------------------------
/crates/cli/src/parser/normalizer.rs:
--------------------------------------------------------------------------------
1 | use super::core::{ArgDetection, CAFILE_FLAG, RAW_FLAG};
2 | use super::headers::HeaderMap;
3 | use super::method;
4 | use super::url;
5 | use crate::core::Flags;
6 | use crate::core::{Error, PushDataItem};
7 | use crate::items::Items;
8 | use crate::request::Method;
9 |
10 | #[cfg_attr(test, derive(Debug))]
11 | pub struct Normalizer {
12 | pub urls: Vec,
13 | method: Option,
14 | pub flags: Flags,
15 | pub headers: HeaderMap,
16 | pub items: Items,
17 | pub raw: Option,
18 | pub certificate_authority_file: Option,
19 | }
20 |
21 | impl Normalizer {
22 | pub fn parse(args: &[String], output_redirected: bool, default_scheme: &str, default_host: &str) -> Result {
23 | let mut method: Option = None;
24 | let mut urls: Vec = Vec::new();
25 | let mut flags = Flags::new(output_redirected);
26 | let mut headers = HeaderMap::new();
27 | let mut items = Items::new();
28 | let mut raw: Option = None;
29 | let mut certificate_authority_file: Option = None;
30 | let args_length = args.len();
31 |
32 | for (arg_index, arg) in args.iter().enumerate().take(args_length) {
33 | if arg_index == 0 {
34 | method = method::from_str(arg);
35 | if method.is_some() {
36 | continue;
37 | }
38 | }
39 |
40 | if (method.is_some() && arg_index == 1) || (method.is_none() && arg_index == 0) {
41 | if arg.is_likely_url() {
42 | urls.push(arg.clone());
43 | continue;
44 | }
45 | } else if arg.is_url() || arg.is_very_likely_url() {
46 | urls.push(arg.clone());
47 | continue;
48 | }
49 |
50 | if arg.is_raw_flag() {
51 | let raw_data = arg[RAW_FLAG.len()..].to_string();
52 | if raw.is_some() {
53 | return Err(Error::TooManyRaw);
54 | }
55 | if !raw_data.is_empty() {
56 | raw = Some(raw_data);
57 | }
58 | } else if arg.is_cafile_flag() {
59 | let cafile = arg[CAFILE_FLAG.len()..].to_string();
60 | if !cafile.is_empty() {
61 | certificate_authority_file = Some(cafile);
62 | }
63 | } else if arg.is_flag() {
64 | flags.push(arg)?;
65 | } else if arg.is_header() {
66 | headers.push(arg)?;
67 | } else if arg.is_item() {
68 | items.push(arg)?;
69 | } else if method.is_none() {
70 | return Err(Error::Unexpected(arg.clone()));
71 | }
72 |
73 | if flags.show_version || flags.show_help {
74 | break;
75 | }
76 | }
77 |
78 | if !flags.http && !flags.https {
79 | flags.http = true;
80 | }
81 |
82 | if !urls.is_empty() {
83 | let scheme = if flags.https {
84 | "https"
85 | } else if flags.http {
86 | "http"
87 | } else {
88 | default_scheme
89 | };
90 | for url in urls.iter_mut() {
91 | *url = url::normalize(url, scheme, default_host);
92 | }
93 | }
94 |
95 | Ok(Normalizer {
96 | urls,
97 | method,
98 | flags,
99 | headers,
100 | items,
101 | raw,
102 | certificate_authority_file,
103 | })
104 | }
105 |
106 | pub fn method(&mut self) -> Method {
107 | let method = self.method.take();
108 | match method {
109 | Some(method) => method,
110 | _ => {
111 | if self.has_input_data() {
112 | Method::POST
113 | } else {
114 | Method::GET
115 | }
116 | }
117 | }
118 | }
119 |
120 | pub fn has_input_data(&self) -> bool {
121 | !self.items.is_empty() || self.raw.is_some()
122 | }
123 | }
124 |
125 | // UNIT TESTS /////////////////////////////////////////////////////////////////////////////
126 |
127 | // FIXME More tests (in particular if output_redirected=true)
128 | #[cfg(test)]
129 | mod tests {
130 | use super::{Error, Normalizer};
131 | const DEFAULT_SCHEME: &str = "http";
132 | const DEFAULT_HOST: &str = "l-o-c-a-l-h-o-s-t";
133 |
134 | macro_rules! assert_one_arg_url_eq {
135 | ($url:expr, $expected:expr) => {
136 | let args: Vec = rh_test::args![$url];
137 | let mut normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST).unwrap();
138 | assert!(normalizer.method() == Method::GET);
139 | assert_eq!(normalizer.urls.len(), 1);
140 | rh_test::assert_str_eq!(normalizer.urls[0], $expected);
141 | };
142 | }
143 |
144 | mod method {
145 | use super::*;
146 | use super::{DEFAULT_HOST, DEFAULT_SCHEME};
147 | use crate::request::Method;
148 |
149 | #[test]
150 | fn standard_method() {
151 | let args = rh_test::args!["HEAD", "localhost"];
152 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST).expect("Cannot parse standard method");
153 | assert_eq!(normalizer.method, Some(Method::HEAD));
154 | assert_eq!(normalizer.urls.len(), 1);
155 | }
156 |
157 | #[test]
158 | fn custom_method() {
159 | let args = rh_test::args!["HELLO", "localhost"];
160 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST).expect("Cannot parse custom method");
161 | assert_eq!(normalizer.method, Some(Method::from_bytes(b"HELLO").unwrap()));
162 | assert_eq!(normalizer.urls.len(), 1);
163 | }
164 |
165 | #[test]
166 | fn no_methods_because_lowercase() {
167 | let args = rh_test::args!["get", "localhost"];
168 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST).expect("Cannot parse multi-urls");
169 | assert_eq!(normalizer.urls.len(), 2);
170 | }
171 | }
172 |
173 | mod urls {
174 | use super::{Error, Normalizer};
175 | use super::{DEFAULT_HOST, DEFAULT_SCHEME};
176 | use crate::request::Method;
177 |
178 | #[test]
179 | fn no_args() {
180 | let args = rh_test::args![];
181 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST).unwrap();
182 | assert_eq!(normalizer.method, None);
183 | assert_eq!(normalizer.urls.len(), 0);
184 | }
185 |
186 | #[test]
187 | fn only_one_url_arg() {
188 | assert_one_arg_url_eq!("http://test.com", "http://test.com");
189 | assert_one_arg_url_eq!("test.com", &format!("{}://test.com", DEFAULT_SCHEME));
190 | assert_one_arg_url_eq!("test", &format!("{}://test", DEFAULT_SCHEME));
191 | }
192 |
193 | #[test]
194 | fn method_and_url() -> Result<(), Error> {
195 | let args = rh_test::args!["GET", "localhost"];
196 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST)?;
197 | assert_eq!(normalizer.urls.len(), 1);
198 | rh_test::assert_str_eq!(normalizer.urls[0], format!("{}://localhost", DEFAULT_SCHEME));
199 | Ok(())
200 | }
201 |
202 | #[test]
203 | fn method_and_url_and_flag() -> Result<(), Error> {
204 | let args = rh_test::args!["GET", "localhost", "--headers"];
205 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST)?;
206 | assert_eq!(normalizer.urls.len(), 1);
207 | rh_test::assert_str_eq!(normalizer.urls[0], format!("{}://localhost", DEFAULT_SCHEME));
208 | Ok(())
209 | }
210 |
211 | #[test]
212 | fn url_and_flag() -> Result<(), Error> {
213 | let args = rh_test::args!["localhost", "--headers"];
214 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST)?;
215 | assert_eq!(normalizer.urls.len(), 1);
216 | rh_test::assert_str_eq!(normalizer.urls[0], format!("{}://localhost", DEFAULT_SCHEME));
217 | Ok(())
218 | }
219 | }
220 |
221 | mod flags {
222 | use super::{Error, Normalizer};
223 | use super::{DEFAULT_HOST, DEFAULT_SCHEME};
224 | use crate::request::Method;
225 |
226 | #[test]
227 | fn force_http() -> Result<(), Error> {
228 | let args: Vec = rh_test::args!["GET", "test.com", "--http"];
229 | let mut normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST)?;
230 | assert!(normalizer.method() == Method::GET);
231 | assert_eq!(normalizer.urls.len(), 1);
232 | rh_test::assert_str_eq!(normalizer.urls[0], "http://test.com");
233 | assert_eq!(normalizer.flags.http, true);
234 | assert_eq!(normalizer.flags.https, false);
235 | Ok(())
236 | }
237 |
238 | #[test]
239 | fn force_https() -> Result<(), Error> {
240 | let args: Vec = rh_test::args!["GET", "test.com", "--https"];
241 | let mut normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST)?;
242 | assert!(normalizer.method() == Method::GET);
243 | assert_eq!(normalizer.urls.len(), 1);
244 | rh_test::assert_str_eq!(normalizer.urls[0], "https://test.com");
245 | assert_eq!(normalizer.flags.http, false);
246 | assert_eq!(normalizer.flags.https, true);
247 | Ok(())
248 | }
249 |
250 | #[test]
251 | fn version() -> Result<(), Error> {
252 | let args: Vec = rh_test::args!["--version"];
253 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST)?;
254 | assert_eq!(normalizer.urls.len(), 0);
255 | assert_eq!(normalizer.method, None);
256 | assert_eq!(normalizer.flags.show_version, true);
257 | Ok(())
258 | }
259 |
260 | #[test]
261 | fn help() -> Result<(), Error> {
262 | let args: Vec = rh_test::args!["--help"];
263 | let normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST)?;
264 | assert_eq!(normalizer.urls.len(), 0);
265 | assert_eq!(normalizer.method, None);
266 | assert_eq!(normalizer.flags.show_help, true);
267 | Ok(())
268 | }
269 | }
270 |
271 | mod raw {
272 | use super::Normalizer;
273 | use super::{DEFAULT_HOST, DEFAULT_SCHEME};
274 | use crate::request::Method;
275 |
276 | #[test]
277 | fn raw_data() {
278 | let args: Vec = rh_test::args!["test.com", "--raw=~data~"];
279 | let mut normalizer = Normalizer::parse(&args, false, DEFAULT_SCHEME, DEFAULT_HOST).unwrap();
280 | assert_eq!(normalizer.method(), Method::POST);
281 | assert_eq!(normalizer.urls.len(), 1);
282 | rh_test::assert_str_eq!(normalizer.urls[0], format!("{}://test.com", DEFAULT_SCHEME));
283 | assert_eq!(normalizer.raw, Some("~data~".to_string()));
284 | assert_eq!(normalizer.flags.as_json, false);
285 | assert_eq!(normalizer.flags.as_form, false);
286 | }
287 | }
288 | }
289 |
--------------------------------------------------------------------------------
/crates/cli/src/parser/url.rs:
--------------------------------------------------------------------------------
1 | use regex::Regex;
2 | use url::Url;
3 |
4 | pub fn normalize(url: &str, default_scheme: &str, default_host: &str) -> String {
5 | let res = Url::parse(url);
6 | if res.is_ok() {
7 | if url.starts_with("http") {
8 | return url.into();
9 | } else {
10 | return format!("{}://{}", default_scheme, url);
11 | }
12 | }
13 | match url {
14 | ":" => format!("{}://{}", default_scheme, default_host),
15 | part if Regex::new(r"^://").unwrap().is_match(part) => {
16 | format!("{}://{}{}", default_scheme, default_host, &part[2..])
17 | }
18 | part if Regex::new(r"^:/").unwrap().is_match(part) => {
19 | format!("{}://{}{}", default_scheme, default_host, &part[1..])
20 | }
21 | part if Regex::new(r"^:\d").unwrap().is_match(part) => {
22 | format!("{}://{}{}", default_scheme, default_host, part)
23 | }
24 | part if Regex::new(r"^/").unwrap().is_match(part) => {
25 | format!("{}://{}{}", default_scheme, default_host, part)
26 | }
27 | _ => {
28 | if url.starts_with('/') {
29 | format!("{}://{}/{}", default_scheme, default_host, url)
30 | } else {
31 | format!("{}://{}", default_scheme, url)
32 | }
33 | }
34 | }
35 | }
36 |
37 | // UNIT TESTS /////////////////////////////////////////////////////////////////////////////
38 |
39 | #[cfg(test)]
40 | mod tests {
41 | use super::normalize;
42 | const DEFAULT_SCHEME: &str = "https";
43 | const DEFAULT_HOST: &str = "l-o-c-a-l-h-o-s-t";
44 |
45 | macro_rules! assert_normalize {
46 | ($url:expr, $expected:expr) => {
47 | assert_eq!(normalize($url, DEFAULT_SCHEME, DEFAULT_HOST), $expected)
48 | };
49 | }
50 |
51 | macro_rules! assert_normalize_with_defaults {
52 | ($url:expr, $expected:expr) => {
53 | assert_eq!(normalize($url, DEFAULT_SCHEME, DEFAULT_HOST), format!($expected, DEFAULT_SCHEME, DEFAULT_HOST))
54 | };
55 | }
56 |
57 | macro_rules! assert_normalize_with_default_scheme {
58 | ($url:expr, $expected:expr) => {
59 | assert_eq!(normalize($url, DEFAULT_SCHEME, DEFAULT_HOST), format!($expected, DEFAULT_SCHEME))
60 | };
61 | }
62 |
63 | macro_rules! assert_valid {
64 | ($url:expr) => {
65 | assert_eq!(normalize($url, DEFAULT_SCHEME, DEFAULT_HOST), $url)
66 | };
67 | }
68 |
69 | #[test]
70 | fn macro_assert_normalise() {
71 | assert_valid!("http://test.com");
72 | assert_normalize!("http://test.com", "http://test.com");
73 | assert_normalize_with_defaults!(format!("{}://{}", DEFAULT_SCHEME, DEFAULT_HOST).as_str(), "{}://{}");
74 | assert_normalize_with_default_scheme!(format!("{}://{}", DEFAULT_SCHEME, "host-example.com").as_str(), "{}://host-example.com");
75 | }
76 |
77 | #[test]
78 | fn host() {
79 | assert_normalize_with_default_scheme!("localhost", "{}://localhost");
80 | assert_normalize_with_default_scheme!("localhost:9200", "{}://localhost:9200");
81 | assert_normalize_with_default_scheme!("test.com", "{}://test.com");
82 | assert_normalize_with_default_scheme!("test.com:9200", "{}://test.com:9200");
83 | assert_normalize_with_default_scheme!("test.com/a?b=c", "{}://test.com/a?b=c");
84 | assert_normalize_with_default_scheme!("test.com:1024/a?b=c", "{}://test.com:1024/a?b=c");
85 | assert_normalize_with_default_scheme!("test.com/a/b/c", "{}://test.com/a/b/c");
86 | assert_normalize_with_default_scheme!("test.com:1024/a/b/c", "{}://test.com:1024/a/b/c");
87 | }
88 |
89 | #[test]
90 | fn default_host() {
91 | assert_normalize_with_defaults!(":", "{}://{}");
92 | assert_normalize_with_defaults!(":/", "{}://{}/");
93 | assert_normalize_with_defaults!(":/uri", "{}://{}/uri");
94 | assert_normalize_with_defaults!("://uri", "{}://{}/uri");
95 | assert_normalize_with_defaults!(":/uri/a/b/c", "{}://{}/uri/a/b/c");
96 | assert_normalize_with_defaults!(":/uri/a/b/c/d.html", "{}://{}/uri/a/b/c/d.html");
97 | assert_normalize_with_defaults!(":9000", "{}://{}:9000");
98 | assert_normalize_with_defaults!(":5000/", "{}://{}:5000/");
99 | assert_normalize_with_defaults!(":2000/uri", "{}://{}:2000/uri");
100 | assert_normalize_with_defaults!("/uri", "{}://{}/uri");
101 | assert_normalize_with_defaults!("/uri/a.jpeg", "{}://{}/uri/a.jpeg");
102 | assert_normalize_with_defaults!(DEFAULT_HOST, "{}://{}");
103 | }
104 |
105 | #[test]
106 | fn proper_urls() {
107 | assert_valid!("http://test.com");
108 | assert_valid!("https://test.com");
109 | assert_valid!("http://test.com:9000");
110 | assert_valid!("https://test.com:9000");
111 | assert_valid!("https://test.com:9000/a/b.html");
112 | assert_valid!("https://test.com:9000/a/b/");
113 | assert_valid!("https://test.com:9000/a/b.html?c=d");
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/crates/cli/src/request/body/form.rs:
--------------------------------------------------------------------------------
1 | use serde_urlencoded::ser::Error;
2 |
3 | use crate::items::Items;
4 |
5 | pub fn serialize(items: &Items) -> Result {
6 | serde_urlencoded::to_string(&items)
7 | }
8 |
--------------------------------------------------------------------------------
/crates/cli/src/request/body/json.rs:
--------------------------------------------------------------------------------
1 | use serde_json::error::Error;
2 |
3 | use crate::items::Items;
4 |
5 | pub fn serialize(items: &Items) -> Result {
6 | serde_json::to_string(&items)
7 | }
8 |
--------------------------------------------------------------------------------
/crates/cli/src/request/body/mod.rs:
--------------------------------------------------------------------------------
1 | mod form;
2 | mod json;
3 |
4 | use crate::core::{Workspace, WorkspaceData};
5 | use reqwest::blocking::RequestBuilder;
6 |
7 | pub trait Body {
8 | fn body_if_items(self, args: &Workspace) -> RequestBuilder;
9 | }
10 |
11 | impl Body for RequestBuilder {
12 | fn body_if_items(self, args: &Workspace) -> RequestBuilder {
13 | match build_body(args) {
14 | Some(body) => self.body(body),
15 | None => self,
16 | }
17 | }
18 | }
19 |
20 | fn build_body(args: &Workspace) -> Option {
21 | if args.has_items() {
22 | if args.is_json() {
23 | Some(json::serialize(&args.items.borrow()).unwrap())
24 | } else {
25 | Some(form::serialize(&args.items.borrow()).unwrap())
26 | }
27 | } else {
28 | args.raw.as_ref().cloned()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/crates/cli/src/request/certificate.rs:
--------------------------------------------------------------------------------
1 | use reqwest::Certificate;
2 |
3 | use crate::core::Result;
4 | use std::io::Read;
5 | use std::{fs::File, path::Path};
6 |
7 | pub fn load>(path: P) -> Result {
8 | let mut buf = Vec::new();
9 | File::open(&path)?.read_to_end(&mut buf)?;
10 | let cert = certificate(path, &buf)?;
11 | Ok(cert)
12 | }
13 |
14 | fn certificate>(path: P, buf: &[u8]) -> Result {
15 | let cert = if Some(std::ffi::OsStr::new("der")) == path.as_ref().extension() {
16 | Certificate::from_der(buf)
17 | } else {
18 | Certificate::from_pem(buf)
19 | }?;
20 | Ok(cert)
21 | }
22 |
--------------------------------------------------------------------------------
/crates/cli/src/request/header.rs:
--------------------------------------------------------------------------------
1 | pub const ACCEPT: &str = "accept";
2 | pub const CONTENT_TYPE: &str = "content-type";
3 | pub const USER_AGENT: &str = "user-agent";
4 |
5 | pub trait StandardHeader {
6 | fn is_standard(&self) -> bool;
7 | }
8 |
9 | impl StandardHeader for reqwest::header::HeaderName {
10 | fn is_standard(&self) -> bool {
11 | let header_name = self.as_str();
12 | [
13 | ACCEPT,
14 | "accept-ch",
15 | "accept-ch-lifetime",
16 | "accept-encoding",
17 | "accept-language",
18 | "accept-push-policy",
19 | "accept-ranges",
20 | "accept-signature",
21 | "access-control-allow-credentials",
22 | "access-control-allow-headers",
23 | "access-control-allow-methods",
24 | "access-control-allow-origin",
25 | "access-control-expose-headers",
26 | "access-control-max-age",
27 | "access-control-request-headers",
28 | "access-control-request-method",
29 | "age",
30 | "allow",
31 | "alt-svc",
32 | "authorization",
33 | "cache-control",
34 | "clear-site-data",
35 | "connection",
36 | "content-disposition",
37 | "content-dpr",
38 | "content-encoding",
39 | "content-language",
40 | "content-length",
41 | "content-location",
42 | "content-range",
43 | "content-security-policy",
44 | "content-security-policy-report-only",
45 | CONTENT_TYPE,
46 | "cookie",
47 | "cookie2",
48 | "cross-origin-embedder-policy",
49 | "cross-origin-opener-policy",
50 | "cross-origin-resource-policy",
51 | "date",
52 | "device-memory",
53 | "downlink",
54 | "dpr",
55 | "early-data",
56 | "ect",
57 | "etag",
58 | "expect",
59 | "expect-ct",
60 | "expires",
61 | "feature-policy",
62 | "forwarded",
63 | "from",
64 | "host",
65 | "if-match",
66 | "if-modified-since",
67 | "if-none-match",
68 | "if-range",
69 | "if-unmodified-since",
70 | "keep-alive",
71 | "large-allocation",
72 | "last-event-id",
73 | "last-modified",
74 | "link",
75 | "location",
76 | "max-forwards",
77 | "nel",
78 | "origin",
79 | "origin-isolation",
80 | "ping-from",
81 | "ping-to",
82 | "pragma",
83 | "proxy-authenticate",
84 | "proxy-authorization",
85 | "public-key-pins",
86 | "public-key-pins-report-only",
87 | "push-policy",
88 | "range",
89 | "referer",
90 | "referrer-policy",
91 | "report-to",
92 | "retry-after",
93 | "rtt",
94 | "save-data",
95 | "sec-ch-ua",
96 | "sec-ch-ua-arch",
97 | "sec-ch-ua-bitness",
98 | "sec-ch-ua-full-version",
99 | "sec-ch-ua-full-version-list",
100 | "sec-ch-ua-mobile",
101 | "sec-ch-ua-model",
102 | "sec-ch-ua-platform",
103 | "sec-ch-ua-platform-version",
104 | "sec-fetch-dest",
105 | "sec-fetch-mode",
106 | "sec-fetch-site",
107 | "sec-fetch-user",
108 | "sec-websocket-accept",
109 | "sec-websocket-extensions",
110 | "sec-websocket-key",
111 | "sec-websocket-protocol",
112 | "sec-websocket-version",
113 | "server",
114 | "server-timing",
115 | "service-worker-allowed",
116 | "set-cookie",
117 | "set-cookie2",
118 | "signature",
119 | "signed-headers",
120 | "sourcemap",
121 | "strict-transport-security",
122 | "te",
123 | "timing-allow-origin",
124 | "trailer",
125 | "transfer-encoding",
126 | "upgrade",
127 | "upgrade-insecure-requests",
128 | USER_AGENT,
129 | "vary",
130 | "via",
131 | "viewport-width",
132 | "warning",
133 | "width",
134 | "www-authenticate",
135 | "x-content-type-options",
136 | "x-dns-prefetch-control",
137 | "x-download-options",
138 | "x-firefox-spdy",
139 | "x-forwarded-for",
140 | "x-forwarded-host",
141 | "x-forwarded-proto",
142 | "x-frame-options",
143 | "x-permitted-cross-domain-policies",
144 | "x-pingback",
145 | "x-powered-by",
146 | "x-requested-with",
147 | "x-robots-tag",
148 | "x-ua-compatible",
149 | "x-xss-protection",
150 | ]
151 | .contains(&header_name)
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/crates/cli/src/request/headers.rs:
--------------------------------------------------------------------------------
1 | use crate::core::{Workspace, WorkspaceData};
2 | use crate::{rh_homepage, rh_name, rh_version};
3 | use reqwest::header::{HeaderMap, HeaderValue};
4 |
5 | use super::header;
6 |
7 | pub fn upgrade(args: &Workspace, headers: &mut HeaderMap) {
8 | if args.is_json() {
9 | if !headers.contains_key(header::CONTENT_TYPE) {
10 | headers.append(header::CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap());
11 | }
12 | if !headers.contains_key(header::ACCEPT) {
13 | headers.append(header::ACCEPT, HeaderValue::from_str("application/json").unwrap());
14 | }
15 | }
16 | if args.is_form() && !headers.contains_key(header::CONTENT_TYPE) {
17 | headers.append(header::CONTENT_TYPE, HeaderValue::from_str("application/x-www-form-urlencoded").unwrap());
18 | }
19 |
20 | if !headers.contains_key(header::USER_AGENT) {
21 | headers.append(
22 | header::USER_AGENT,
23 | HeaderValue::from_str(&format!("{}/{} {}", rh_name!(), rh_version!(), rh_homepage!(),)).unwrap(),
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/crates/cli/src/request/mod.rs:
--------------------------------------------------------------------------------
1 | mod body;
2 | mod certificate;
3 |
4 | pub(crate) mod header;
5 | pub(crate) mod headers;
6 | use crate::core::{Error, Result, Workspace};
7 | use body::Body;
8 | use std::time::Duration;
9 |
10 | pub type Response = reqwest::blocking::Response;
11 | pub type Method = reqwest::Method;
12 | pub type HeaderMap = reqwest::header::HeaderMap;
13 |
14 | pub fn execute(args: &Workspace, req_number: u8, headers: &HeaderMap) -> Result {
15 | let mut client_builder = reqwest::blocking::Client::builder()
16 | .default_headers(headers.clone())
17 | .gzip(false)
18 | .timeout(Duration::from_secs(10));
19 |
20 | if let Some(cafile) = args.certificate_authority_file.as_ref() {
21 | let cert = certificate::load(cafile)?;
22 | client_builder = client_builder.add_root_certificate(cert);
23 | }
24 |
25 | let client = client_builder.build()?;
26 | let method = args.method.clone();
27 | let url = &args.urls[req_number as usize];
28 | let response = client.request(method, url).body_if_items(args).send()?;
29 | Ok(response)
30 | }
31 |
32 | impl From for Error {
33 | fn from(err: reqwest::Error) -> Error {
34 | Error::Request(err.to_string())
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/crates/cli/src/shell/error.rs:
--------------------------------------------------------------------------------
1 | use super::{Render, enable_colors};
2 | use crate::theme::style::Color;
3 | use std::{
4 | fmt::Display,
5 | io::{Result, Write},
6 | };
7 |
8 | pub struct ErrorRender {
9 | message: T,
10 | }
11 |
12 | impl ErrorRender {
13 | pub fn new(message: T) -> Self {
14 | Self { message }
15 | }
16 | }
17 |
18 | impl Render for ErrorRender {
19 | #[inline]
20 | fn is_style_active(&self) -> bool {
21 | enable_colors()
22 | }
23 |
24 | #[inline]
25 | fn write(&self, writer: &mut W) -> Result<()> {
26 | self.write_with_style(writer, "Error: ".as_bytes(), &Color::Red.bold())?;
27 | writeln!(writer, "{}", self.message)?;
28 | Ok(())
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/crates/cli/src/shell/form.rs:
--------------------------------------------------------------------------------
1 | use super::Render;
2 | use serde::Serialize;
3 | use std::io::{Result, Write};
4 |
5 | pub struct FormRender<'a, T> {
6 | value: &'a T,
7 | // compact: bool,
8 | style_enabled: bool,
9 | }
10 |
11 | impl<'a, T: Serialize> FormRender<'a, T> {
12 | pub fn new(value: &'a T, _compact: bool, style_enabled: bool) -> Self {
13 | Self {
14 | value,
15 | // compact,
16 | style_enabled,
17 | }
18 | }
19 | }
20 |
21 | impl<'a, T: Serialize> Render for FormRender<'a, T> {
22 | #[inline]
23 | fn is_style_active(&self) -> bool {
24 | self.style_enabled
25 | }
26 |
27 | #[inline]
28 | fn write(&self, writer: &mut W) -> Result<()> {
29 | match serde_urlencoded::to_string(self.value) {
30 | Ok(buffer) => {
31 | writer.write_all(buffer.as_bytes())?;
32 | }
33 | Err(_) => {
34 | writer.write_all(b"Can't render the items as URL encoded")?;
35 | }
36 | };
37 | Ok(())
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/crates/cli/src/shell/json.rs:
--------------------------------------------------------------------------------
1 | use super::Render;
2 | use ansi_term::{Color as AnsiTermColor, Style};
3 | use colored_json::{ColoredFormatter, CompactFormatter, PrettyFormatter, Styler};
4 | use serde::Serialize;
5 | use serde_json::ser::Formatter;
6 | use std::io::{Result, Write};
7 |
8 | pub struct JsonRender<'a, T> {
9 | value: &'a T,
10 | compact: bool,
11 | style_enabled: bool,
12 | }
13 |
14 | impl<'a, T: Serialize> JsonRender<'a, T> {
15 | pub fn new(value: &'a T, compact: bool, style_enabled: bool) -> Self {
16 | Self { value, compact, style_enabled }
17 | }
18 | }
19 |
20 | impl<'a, T: Serialize> Render for JsonRender<'a, T> {
21 | #[inline]
22 | fn is_style_active(&self) -> bool {
23 | self.style_enabled
24 | }
25 |
26 | #[inline]
27 | fn write(&self, writer: &mut W) -> Result<()> {
28 | if self.compact {
29 | self.write_with_formatter(writer, CompactFormatter)
30 | } else {
31 | self.write_with_formatter(writer, PrettyFormatter::new())
32 | }
33 | }
34 | }
35 |
36 | impl<'a, T: Serialize> JsonRender<'a, T> {
37 | #[inline]
38 | fn write_with_formatter(&self, writer: W, formatter: F) -> Result<()> {
39 | let formatter = ColoredFormatter::with_styler(formatter, self.style());
40 | let mut serializer = serde_json::Serializer::with_formatter(writer, formatter);
41 | self.value.serialize(&mut serializer)?;
42 | Ok(())
43 | }
44 |
45 | #[inline]
46 | fn style(&self) -> Styler {
47 | Styler {
48 | object_brackets: Style::new(),
49 | array_brackets: Style::new().fg(AnsiTermColor::Red),
50 | key: Style::new().fg(AnsiTermColor::Blue),
51 | string_value: Style::new().fg(AnsiTermColor::Green),
52 | integer_value: Style::new().fg(AnsiTermColor::Purple),
53 | float_value: Style::new().fg(AnsiTermColor::Purple),
54 | bool_value: Style::new().fg(AnsiTermColor::Yellow),
55 | nil_value: Style::new().fg(AnsiTermColor::Cyan),
56 | string_include_quotation: true,
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/crates/cli/src/shell/mod.rs:
--------------------------------------------------------------------------------
1 | pub(crate) mod error;
2 | pub(crate) mod form;
3 | pub(crate) mod json;
4 | pub mod os;
5 | pub(crate) mod stream;
6 |
7 | use self::os::OsDirs;
8 | use crate::theme::style::{Color, Style};
9 | use ansi_term::Color as AnsiTermColor;
10 | use ansi_term::Style as AnsiTermStyle;
11 | use std::io::{Result, Write};
12 |
13 | #[inline]
14 | pub fn enable_colors() -> bool {
15 | #[cfg(windows)]
16 | return ansi_term::enable_ansi_support().is_ok();
17 | #[cfg(not(windows))]
18 | true
19 | }
20 |
21 | pub struct Shell<'a, OD, O, E> {
22 | os_dirs: &'a OD,
23 | out: O,
24 | err: E,
25 | }
26 |
27 | impl<'a, OD: OsDirs, O: Write, E: Write> Shell<'a, OD, O, E> {
28 | pub fn new(os_dirs: &'a OD, out: O, err: E) -> Self {
29 | Self { os_dirs, out, err }
30 | }
31 |
32 | pub fn out(&mut self, render: R) -> Result<()> {
33 | render.write(&mut self.out)?;
34 | Ok(())
35 | }
36 | pub fn err(&mut self, render: R) -> Result<()> {
37 | render.write(&mut self.err)?;
38 | Ok(())
39 | }
40 |
41 | pub fn os_dirs(&self) -> &OD {
42 | self.os_dirs
43 | }
44 |
45 | #[inline]
46 | pub fn enable_colors(&self) -> bool {
47 | enable_colors()
48 | }
49 |
50 | // pub fn flush(&mut self) -> Result<()> {
51 | // self.out.flush()?;
52 | // self.err.flush()
53 | // }
54 | }
55 |
56 | pub trait Render {
57 | fn write(&self, writer: &mut W) -> Result<()>;
58 |
59 | fn is_style_active(&self) -> bool;
60 |
61 | fn write_newline(&self, writer: &mut W) -> Result<()> {
62 | writer.write_all(b"\n")?;
63 | Ok(())
64 | }
65 |
66 | fn write_with_style(&self, writer: &mut W, buf: &[u8], style: &Style) -> Result<()> {
67 | if self.is_style_active() {
68 | AnsiTermStyle {
69 | is_bold: style.is_bold,
70 | is_dimmed: style.is_dimmed,
71 | foreground: to_ansi_term_color(style.forecolor),
72 | ..AnsiTermStyle::default()
73 | }
74 | .paint(buf)
75 | .write_to(writer)?;
76 | } else {
77 | writer.write_all(buf)?;
78 | }
79 |
80 | Ok(())
81 | }
82 | }
83 |
84 | #[inline]
85 | fn to_ansi_term_color(color: Option) -> Option {
86 | color.map(|color| color.into())
87 | }
88 |
89 | impl From for AnsiTermColor {
90 | fn from(color: Color) -> AnsiTermColor {
91 | match color {
92 | Color::Black => AnsiTermColor::Black,
93 | Color::Red => AnsiTermColor::Red,
94 | Color::Green => AnsiTermColor::Green,
95 | Color::Yellow => AnsiTermColor::Yellow,
96 | Color::Blue => AnsiTermColor::Blue,
97 | Color::Purple => AnsiTermColor::Purple,
98 | Color::Cyan => AnsiTermColor::Cyan,
99 | Color::White => AnsiTermColor::White,
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/crates/cli/src/shell/os.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 |
3 | use crate::rh_name;
4 |
5 | pub trait OsDirs {
6 | fn app_path(&self, filename: &str) -> Option;
7 | fn app_config_directory(&self) -> Option;
8 | fn config_directory(&self) -> Option;
9 | }
10 |
11 | #[derive(Default)]
12 | pub struct DefaultOsDirs;
13 |
14 | impl OsDirs for DefaultOsDirs {
15 | fn app_path(&self, filename: &str) -> Option {
16 | self.app_config_directory().map(|path| path.join(filename))
17 | }
18 |
19 | fn app_config_directory(&self) -> Option {
20 | self.config_directory().map(|path| path.join(rh_name!()))
21 | }
22 |
23 | fn config_directory(&self) -> Option {
24 | dirs::config_dir()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/crates/cli/src/shell/stream.rs:
--------------------------------------------------------------------------------
1 | use atty::{is, Stream};
2 |
3 | pub fn is_stdout() -> bool {
4 | is(Stream::Stdout)
5 | }
6 |
7 | pub fn is_stdin() -> bool {
8 | is(Stream::Stdin)
9 | }
10 |
--------------------------------------------------------------------------------
/crates/cli/src/test/alias.rs:
--------------------------------------------------------------------------------
1 | use super::os::app_config_directory_for_tests_only;
2 | use crate::commands::alias::storage::{ALIAS_FILENAME_PREFIX, ALIAS_FILENAME_SUFFIX, DEFAULT_ALIAS_NAME};
3 | use std::io::Write;
4 | use std::{
5 | fs::{self, File},
6 | path::Path,
7 | };
8 |
9 | pub const CUSTOM_ALIAS_NAME_1: &str = "alias-one";
10 | pub const CUSTOM_ALIAS_NAME_2: &str = "alias2";
11 | pub const EMPTY_ALIAS_NAME: &str = "empty";
12 |
13 | pub fn alias_filename(name: &str) -> String {
14 | format!(
15 | "{}/{}{}{}",
16 | app_config_directory_for_tests_only().display(),
17 | ALIAS_FILENAME_PREFIX,
18 | name,
19 | ALIAS_FILENAME_SUFFIX
20 | )
21 | }
22 |
23 | pub fn create_alias_file(name: &str) {
24 | create_alias_file_with_args(name, "-v\n-c");
25 | }
26 |
27 | pub fn create_alias_file_with_args(name: &str, args: &str) {
28 | let app_config_path = app_config_directory_for_tests_only();
29 | if !Path::exists(&app_config_path) {
30 | fs::create_dir(app_config_path).expect("Cannot create the app config directory");
31 | }
32 | let mut file = File::create(alias_filename(name)).expect("Cannot create the alias file");
33 | file.write_all(args.as_bytes()).expect("Cannot write content in the alias file");
34 | }
35 |
36 | pub fn create_empty_alias_file(name: &str) {
37 | let mut file = File::create(alias_filename(name)).expect("Cannot create the alias file");
38 | file.write_all("".as_bytes()).expect("Cannot write content in the alias file");
39 | }
40 |
41 | pub fn alias_exists(name: &str) -> bool {
42 | Path::new(&alias_filename(name)).exists()
43 | }
44 |
45 | pub fn delete_alias_file(name: &str) {
46 | let _ = fs::remove_file(alias_filename(name));
47 | }
48 |
49 | pub fn setup() {
50 | std::env::set_var("HOME", app_config_directory_for_tests_only());
51 | delete_alias_file(DEFAULT_ALIAS_NAME);
52 | delete_alias_file(CUSTOM_ALIAS_NAME_1);
53 | delete_alias_file(CUSTOM_ALIAS_NAME_2);
54 | delete_alias_file(EMPTY_ALIAS_NAME);
55 | }
56 |
--------------------------------------------------------------------------------
/crates/cli/src/test/mod.rs:
--------------------------------------------------------------------------------
1 | pub(crate) mod alias;
2 | pub(crate) mod os;
3 |
4 | // #[macro_export]
5 | // macro_rules! args {
6 | // () => {{
7 | // let v = Vec::::new();
8 | // v
9 | // }};
10 | // ($($elem:expr),+ $(,)?) => {{
11 | // let v = vec![
12 | // $( String::from($elem), )*
13 | // ];
14 | // v
15 | // }};
16 | // }
17 |
18 | // #[macro_export]
19 | // macro_rules! arg_alias {
20 | // ($alias:expr) => {
21 | // format!("{}{}", ALIAS_NAME_PREFIX, $alias)
22 | // };
23 | // }
24 |
25 | // #[macro_export]
26 | // macro_rules! assert_str_eq {
27 | // ($url:expr, $expected:expr) => {
28 | // assert_eq!($url, $expected.to_string())
29 | // };
30 | // }
31 |
32 | // mod basics {
33 | // #[test]
34 | // fn macro_args() {
35 | // let args = args![];
36 | // let expected: Vec = vec![];
37 | // assert_eq!(args, expected);
38 |
39 | // let args = args!["one", "two", "three"];
40 | // let expected: Vec = vec!["one".into(), "two".into(), "three".into()];
41 | // assert_eq!(args, expected);
42 | // }
43 |
44 | // #[test]
45 | // fn macro_assert_url_eq() {
46 | // let url = "http://test.com";
47 | // assert_str_eq!(url.to_string(), url);
48 | // }
49 | // }
50 |
--------------------------------------------------------------------------------
/crates/cli/src/test/os.rs:
--------------------------------------------------------------------------------
1 | use std::{env, path::PathBuf};
2 |
3 | use crate::shell::os::OsDirs;
4 |
5 | const APP_NAME_FOR_TESTS_ONLY: &str = "rh-test";
6 |
7 | pub fn app_config_directory_for_tests_only() -> PathBuf {
8 | config_directory_for_tests_only().join(APP_NAME_FOR_TESTS_ONLY)
9 | }
10 | pub fn config_directory_for_tests_only() -> PathBuf {
11 | env::temp_dir()
12 | }
13 |
14 | pub struct TestValidOsDirs;
15 |
16 | impl TestValidOsDirs {
17 | pub fn new() -> Self {
18 | Self {}
19 | }
20 | }
21 |
22 | impl OsDirs for TestValidOsDirs {
23 | fn app_path(&self, filename: &str) -> Option {
24 | self.app_config_directory().map(|path| path.join(filename))
25 | }
26 |
27 | fn app_config_directory(&self) -> Option {
28 | self.config_directory().map(|path| path.join(APP_NAME_FOR_TESTS_ONLY))
29 | }
30 |
31 | fn config_directory(&self) -> Option {
32 | Some(config_directory_for_tests_only())
33 | }
34 | }
35 |
36 | pub struct TestNoOsDirs;
37 |
38 | impl TestNoOsDirs {
39 | pub fn new() -> Self {
40 | Self {}
41 | }
42 | }
43 |
44 | impl OsDirs for TestNoOsDirs {
45 | fn app_path(&self, filename: &str) -> Option {
46 | self.app_config_directory().map(|path| path.join(filename))
47 | }
48 |
49 | fn app_config_directory(&self) -> Option {
50 | self.config_directory()
51 | }
52 |
53 | fn config_directory(&self) -> Option {
54 | None
55 | }
56 | }
57 |
58 | pub struct TestInvalidOsDirs;
59 |
60 | impl TestInvalidOsDirs {
61 | pub fn new() -> Self {
62 | Self {}
63 | }
64 | }
65 |
66 | impl OsDirs for TestInvalidOsDirs {
67 | fn app_path(&self, filename: &str) -> Option {
68 | self.app_config_directory().map(|path| path.join(filename))
69 | }
70 |
71 | fn app_config_directory(&self) -> Option {
72 | self.config_directory()
73 | }
74 |
75 | fn config_directory(&self) -> Option {
76 | Some(PathBuf::from("a-dir-that_does-not-exist"))
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/crates/cli/src/theme/default.rs:
--------------------------------------------------------------------------------
1 | use super::{
2 | style::{Color, Style},
3 | DirectionTheme, HeaderTheme, RequestTheme, ResponseTheme, Theme,
4 | };
5 |
6 | #[cfg_attr(test, derive(Debug))]
7 | #[derive(Clone, Copy)]
8 | pub struct DefaultTheme {}
9 | #[derive(Clone, Copy)]
10 | pub struct DefaultReponseTheme {}
11 | #[derive(Clone, Copy)]
12 | pub struct DefaultRequestTheme {}
13 |
14 | impl RequestTheme for DefaultRequestTheme {
15 | fn as_header(&self) -> &dyn HeaderTheme {
16 | self
17 | }
18 | fn as_direction(&self) -> &dyn DirectionTheme {
19 | self
20 | }
21 | fn primary(&self) -> Style {
22 | Color::Purple.normal()
23 | }
24 | fn secondary(&self) -> Style {
25 | Color::Purple.normal()
26 | }
27 | fn method(&self) -> Style {
28 | Color::Purple.bold()
29 | }
30 | fn url(&self) -> Style {
31 | Color::Purple.normal_newline()
32 | }
33 | }
34 | impl HeaderTheme for DefaultRequestTheme {
35 | fn header_name(&self, standard: bool) -> Style {
36 | crate::ifelse!(standard, self.primary(), self.secondary())
37 | }
38 | fn header_value(&self, _: bool) -> Style {
39 | Style::newline()
40 | }
41 | }
42 | impl DirectionTheme for DefaultRequestTheme {
43 | fn direction(&self, standard: bool) -> Style {
44 | crate::ifelse!(standard, self.primary(), self.secondary())
45 | }
46 | }
47 |
48 | impl ResponseTheme for DefaultReponseTheme {
49 | fn as_header(&self) -> &dyn HeaderTheme {
50 | self
51 | }
52 | fn as_direction(&self) -> &dyn DirectionTheme {
53 | self
54 | }
55 | fn primary(&self) -> Style {
56 | Color::Green.normal()
57 | }
58 | fn secondary(&self) -> Style {
59 | Color::Cyan.normal()
60 | }
61 | fn version(&self) -> Style {
62 | Color::Green.normal()
63 | }
64 | fn status(&self) -> Style {
65 | Color::Green.bold_newline()
66 | }
67 | }
68 | impl HeaderTheme for DefaultReponseTheme {
69 | fn header_name(&self, standard: bool) -> Style {
70 | crate::ifelse!(standard, self.primary(), self.secondary())
71 | }
72 | fn header_value(&self, _: bool) -> Style {
73 | Style::newline()
74 | }
75 | }
76 | impl DirectionTheme for DefaultReponseTheme {
77 | fn direction(&self, standard: bool) -> Style {
78 | crate::ifelse!(standard, self.primary(), self.secondary())
79 | }
80 | }
81 |
82 | impl Theme for DefaultTheme {
83 | fn request(&self) -> Box {
84 | Box::new(DefaultRequestTheme {})
85 | }
86 | fn response(&self) -> Box {
87 | Box::new(DefaultReponseTheme {})
88 | }
89 | }
90 |
91 | impl DefaultTheme {
92 | pub fn new() -> DefaultTheme {
93 | DefaultTheme {}
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/crates/cli/src/theme/mod.rs:
--------------------------------------------------------------------------------
1 | pub(crate) mod default;
2 | pub(crate) mod style;
3 |
4 | use style::Style;
5 |
6 | pub trait Theme {
7 | fn request(&self) -> Box;
8 | fn response(&self) -> Box;
9 | }
10 |
11 | pub trait DirectionTheme {
12 | fn direction(&self, standard: bool) -> Style;
13 | }
14 |
15 | pub trait HeaderTheme {
16 | fn header_name(&self, standard: bool) -> Style;
17 | fn header_value(&self, standard: bool) -> Style;
18 | }
19 |
20 | pub trait RequestTheme: HeaderTheme + DirectionTheme {
21 | fn as_header(&self) -> &dyn HeaderTheme;
22 | fn as_direction(&self) -> &dyn DirectionTheme;
23 | fn primary(&self) -> Style;
24 | fn secondary(&self) -> Style;
25 | fn method(&self) -> Style;
26 | fn url(&self) -> Style;
27 | }
28 |
29 | pub trait ResponseTheme: HeaderTheme + DirectionTheme {
30 | fn as_header(&self) -> &dyn HeaderTheme;
31 | fn as_direction(&self) -> &dyn DirectionTheme;
32 | fn primary(&self) -> Style;
33 | fn secondary(&self) -> Style;
34 | fn version(&self) -> Style;
35 | fn status(&self) -> Style;
36 | }
37 |
38 | #[cfg(test)]
39 | impl core::fmt::Debug for dyn Theme {
40 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
41 | write!(f, "Theme")
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/crates/cli/src/theme/style.rs:
--------------------------------------------------------------------------------
1 | #[derive(Default)]
2 | pub struct Style {
3 | pub forecolor: Option,
4 | pub backcolor: Option,
5 | pub is_bold: bool,
6 | pub is_dimmed: bool,
7 | pub newline: bool,
8 | }
9 |
10 | #[allow(dead_code)]
11 | #[derive(Clone, Copy)]
12 | pub enum Color {
13 | Black,
14 | Red,
15 | Green,
16 | Yellow,
17 | Blue,
18 | Purple,
19 | Cyan,
20 | White,
21 | }
22 |
23 | impl Style {
24 | pub fn newline() -> Style {
25 | Style {
26 | newline: true,
27 | ..Default::default()
28 | }
29 | }
30 | }
31 |
32 | impl Color {
33 | pub fn normal(self) -> Style {
34 | Style {
35 | forecolor: Some(self),
36 | ..Default::default()
37 | }
38 | }
39 | pub fn normal_newline(self) -> Style {
40 | Style {
41 | forecolor: Some(self),
42 | newline: true,
43 | ..Default::default()
44 | }
45 | }
46 | pub fn bold(self) -> Style {
47 | Style {
48 | forecolor: Some(self),
49 | is_bold: true,
50 | ..Default::default()
51 | }
52 | }
53 | pub fn bold_newline(self) -> Style {
54 | Style {
55 | forecolor: Some(self),
56 | is_bold: true,
57 | newline: true,
58 | ..Default::default()
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/crates/cli/tests/certificate.rs:
--------------------------------------------------------------------------------
1 | // #[cfg(test)]
2 | // mod tests {
3 |
4 | // // #[test]
5 | // // fn invalid_pem() {
6 | // // let res = certificate("ca.pem", b"invalid pem");
7 | // // assert!(res.is_err());
8 | // // }
9 |
10 | // #[test]
11 | // fn invalid_der() {
12 | // let res = certificate("ca.der", b"invalid der").unwrap();
13 | // // assert!(res);
14 | // }
15 | // }
16 |
--------------------------------------------------------------------------------
/crates/cli/tests/macros.rs:
--------------------------------------------------------------------------------
1 | #[macro_export]
2 | macro_rules! args {
3 | () => {{
4 | let v = Vec::::new();
5 | v
6 | }};
7 | ($($elem:expr),+ $(,)?) => {{
8 | let v = vec![
9 | $( String::from($elem), )*
10 | ];
11 | v
12 | }};
13 | }
14 |
15 | #[macro_export]
16 | macro_rules! arg_alias {
17 | ($alias:expr) => {
18 | format!("{}{}", ALIAS_NAME_PREFIX, $alias)
19 | };
20 | }
21 |
22 | #[macro_export]
23 | macro_rules! assert_str_eq {
24 | ($url:expr, $expected:expr) => {
25 | assert_eq!($url, $expected.to_string())
26 | };
27 | }
28 |
29 | mod basics {
30 | #[test]
31 | fn macro_args() {
32 | let args = args![];
33 | let expected: Vec = vec![];
34 | assert_eq!(args, expected);
35 |
36 | let args = args!["one", "two", "three"];
37 | let expected: Vec = vec!["one".into(), "two".into(), "three".into()];
38 | assert_eq!(args, expected);
39 | }
40 |
41 | #[test]
42 | fn macro_assert_url_eq() {
43 | let url = "http://test.com";
44 | assert_str_eq!(url.to_string(), url);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/crates/cli/tests/rh.rs:
--------------------------------------------------------------------------------
1 | use httpmock::prelude::*;
2 | use rh::shell::{
3 | os::{DefaultOsDirs, OsDirs},
4 | Shell,
5 | };
6 |
7 | fn shell<'a, OD: OsDirs>(os_dirs: &'a OD) -> Shell<'a, OD, Vec, Vec> {
8 | let out = Vec::new();
9 | let err = Vec::new();
10 | Shell::new(os_dirs, out, err)
11 | }
12 |
13 | #[test]
14 | fn no_args() {
15 | let os_dirs = DefaultOsDirs::default();
16 | let mut shell = shell(&os_dirs);
17 |
18 | let mut args = rh_test::args![];
19 | let exit_code = rh::run(&mut args, &mut shell);
20 | assert_eq!(exit_code, 100);
21 | }
22 |
23 | #[test]
24 | fn get_localhost() {
25 | let server = MockServer::start();
26 | let http_mock = server.mock(|when, then| {
27 | when.path("/");
28 | then.status(200).header("content-type", "text/plain").body("ohi");
29 | });
30 | let url = server.url("/");
31 |
32 | let os_dirs = DefaultOsDirs::default();
33 | let mut shell = shell(&os_dirs);
34 |
35 | let mut args = rh_test::args![url];
36 | let exit_code = rh::run(&mut args, &mut shell);
37 | assert_eq!(exit_code, 0);
38 | http_mock.assert();
39 | }
40 |
--------------------------------------------------------------------------------
/crates/test/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "rh_test"
3 | version = "0.1.0"
4 | edition = "2021"
5 | authors = ["twigly"]
6 | license = "MIT"
7 | description = "A user-friendly command-line tool to request HTTP APis"
8 | readme = "README.md"
9 | homepage = "https://github.com/twigly/rh"
10 | repository = "https://github.com/twigly/rh"
11 | keywords = ["cli", "http", "terminal", "tool", "devops"]
12 | categories = ["command-line-utilities"]
13 |
14 | [dependencies]
15 |
--------------------------------------------------------------------------------
/crates/test/README.md:
--------------------------------------------------------------------------------
1 | # Rust HTTP library for tests
2 |
3 | Used by unit and integration tests.
4 |
--------------------------------------------------------------------------------
/crates/test/src/lib.rs:
--------------------------------------------------------------------------------
1 | #[macro_export]
2 | macro_rules! args {
3 | () => {{
4 | let v = Vec::::new();
5 | v
6 | }};
7 | ($($elem:expr),+ $(,)?) => {{
8 | let v = vec![
9 | $( String::from($elem), )*
10 | ];
11 | v
12 | }};
13 | }
14 |
15 | #[macro_export]
16 | macro_rules! arg_alias {
17 | ($alias:expr) => {
18 | format!("{}{}", ALIAS_NAME_PREFIX, $alias)
19 | };
20 | }
21 |
22 | #[macro_export]
23 | macro_rules! assert_str_eq {
24 | ($url:expr, $expected:expr) => {
25 | assert_eq!($url, $expected.to_string())
26 | };
27 | }
28 |
29 | mod basics {
30 | #[test]
31 | fn macro_args() {
32 | let args = args![];
33 | let expected: Vec = vec![];
34 | assert_eq!(args, expected);
35 |
36 | let args = args!["one", "two", "three"];
37 | let expected: Vec = vec!["one".into(), "two".into(), "three".into()];
38 | assert_eq!(args, expected);
39 | }
40 |
41 | #[test]
42 | fn macro_assert_url_eq() {
43 | let url = "http://test.com";
44 | assert_str_eq!(url.to_string(), url);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/doc/alias.md:
--------------------------------------------------------------------------------
1 | # Alias
2 |
3 | ## How to configure?
4 |
5 | You can change ```rh``` default behaviour and create aliases you can reuse easily.
6 |
7 | To see all the aliases available, the syntax is:
8 |
9 | ```bash
10 | > rh alias --list
11 | ```
12 |
13 | To update a configuration, the syntax is:
14 |
15 | ```bash
16 | > rh alias [@alias]
17 | ```
18 |
19 | Please note that ```@alias``` is optional. If not specified it's the ```default``` alias. ```@alias``` must be lower-case.
20 |
21 | ```options``` can be any ```rh``` options.
22 |
23 | ## Default alias
24 |
25 | The ```default``` alias is used if no alias is specified. For example, if you want to show the response headers (```--header``` or ```-h```):
26 |
27 | ```bash
28 | > rh alias -h
29 | ```
30 |
31 | You can select multiple options at the same time:
32 |
33 | ```bash
34 | > rh alias --header --compact
35 | ```
36 |
37 | Or the same but shorter:
38 |
39 | ```bash
40 | > rh alias -hc
41 | ```
42 |
43 | ## Custom alias
44 |
45 | You can create an alias to show the ```-U```RL and method + to show the response ```-h```eaders + to show a ```-c```ompact response:
46 |
47 | ```bash
48 | > rh alias @my-alias -Uhc
49 | ```
50 |
51 | ## How to use an alias
52 |
53 | You can use the "my-alias" alias created above to show the URL, method, response headers, and compact the response body:
54 |
55 | ```bash
56 | > rh @my-alias https://httpbin.org/image/jpeg
57 | ```
58 |
59 | You can use also the previous default alias that was built with the options ```-hc```:
60 |
61 | ```bash
62 | > rh https://httpbin.org/image/jpeg
63 | ```
64 |
65 | ## Delete an alias
66 |
67 | You can delete any alias you created, including the default alias. To delete the default alias:
68 |
69 | ```bash
70 | > rh alias --delete
71 | ```
72 |
73 | To delete the alias "my-alias":
74 |
75 | ```bash
76 | > rh alias --delete @my-alias
77 | ```
78 |
79 | ## List all aliases
80 |
81 | As simple as:
82 |
83 | ```bash
84 | > rh alias --list
85 | ```
86 |
87 | ## More options in the future
88 |
89 | The following default options are not available yet. Once available, these options will be available in ```rh``` in order for the aliases to be more flexible:
90 |
91 | ```
92 | --hostname=localhost
93 | --port=80
94 | --secure-port=443
95 | --method=GET
96 | --method-if-body=POST
97 | --method-if-pipe=POST
98 | ```
99 |
--------------------------------------------------------------------------------
/doc/authentication.md:
--------------------------------------------------------------------------------
1 | # Authentication
2 |
3 | Feature not available yet.
4 |
5 | ```bash
6 | > rh --basic=my-user:my-pass https://httpbin.org/basic-auth/my-user/my-pass
7 | ```
8 |
9 | ```bash
10 | > rh --digest=my-user:my-pass httpbin.org/digest-auth/rh/my-user/my-pass
11 | ```
12 |
13 | ```bash
14 | > rh --bearer=token https://httpbin.org/bearer
15 | ```
16 |
--------------------------------------------------------------------------------
/doc/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thanks for considering helping the ```rh``` project. There are many ways you can help: using ```rh``` and [reporting bugs](https://github.com/twigly/rh/issues), making additions and improvements to ```rh```, documentation, logo, package manager...
4 |
5 | ## Reporting bugs
6 |
7 | Please file a [GitHub issue](https://github.com/twigly/rh/issues). Include as much information as possible.
8 |
9 | Feel free to file [GitHub issues](https://github.com/twigly/rh/issues) to get help, or ask a question.
10 |
11 | ## Code changes
12 |
13 | Some ideas and guidelines for contributions:
14 |
15 | - For large features, you can file an issue prior to starting work.
16 | - Feel free to submit a PR even if the work is not totally finished, for feedback or to hand-over.
17 | - Some [ideas here](todo.md)
18 |
19 | ## Package managers
20 |
21 | - Having ```rh``` available on various platform would be great
22 |
23 | ## Any help is welcome
24 |
25 | He's not developing anything because he's busy sleeping 20 hours a day, but he helps his way...
26 |
27 | 
28 |
--------------------------------------------------------------------------------
/doc/examples.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | In the following examples, you can add the flags ```-uhH``` to show more details.
4 |
5 | - ```-u``` to show the URL and method
6 | - ```-h``` to show the response headers
7 | - ```-H``` to show the request headers
8 |
9 | More information with:
10 |
11 | ```bash
12 | > rh --help
13 | ```
14 |
15 | ## Basics
16 |
17 | Let's start with "Hello, World!":
18 |
19 | ```bash
20 | > rh httpbin.org/get
21 | ```
22 |
23 | You can POST a request (```rh``` will default to POST because there is a body):
24 |
25 | ```bash
26 | > rh httpbin.org/post id=rh
27 | ```
28 |
29 | A POST request with headers but no body:
30 |
31 | ```bash
32 | > rh POST httpbin.org/post X-key1:true X-key2:true
33 | ```
34 |
35 | ## Headers and items
36 |
37 | The separator ```:``` is used to create headers:
38 |
39 | ```bash
40 | > rh httpbin.org/get key:Value
41 | ```
42 |
43 | The separator ```=``` is used to create items to POST (if there are items then the method is POST):
44 |
45 | ```bash
46 | > rh httpbin.org/post key=Value
47 | ```
48 |
49 | ## Localhost
50 |
51 | To run the examples of this "localhost" section you need a local server. In the following examples, if you don't specify a host, ```localhost``` will be the default host. Once the config feature is available, you'll be able to change the default host.
52 |
53 | Basic:
54 |
55 | ```bash
56 | > rh http://localhost/test
57 | ```
58 |
59 | Don't be bothered with the localhost domain:
60 |
61 | ```bash
62 | > rh /test
63 | ```
64 |
65 | Or :
66 |
67 | ```bash
68 | > rh :
69 | ```
70 |
71 | Localhost with a particular port:
72 |
73 | ```bash
74 | > rh :9200
75 | ```
76 |
77 | ```bash
78 | > rh :9200/_cluster/health
79 | ```
80 |
81 | ## Config (not available yet)
82 |
83 | You can create a config named ```dev``` (this config says to POST the body ```id=rh``` to ```httpbin.org/post```):
84 |
85 | ```bash
86 | > rh dev httpbin.org/post id=rh
87 | ```
88 |
89 | Let's say you have Elasticsearch running on the ```elasticsearch``` domain, you can define the following config ```ei``` (that would stand for Elasticsearch Indices):
90 |
91 | ```bash
92 | > rh config ei elasticsearch:9200/_cat/indices/*,-.*?v&s=index
93 | ```
94 |
95 | Then you can just run the following command to show the Elasticsearch indices:
96 |
97 | ```bash
98 | > rh ei
99 | ```
100 |
101 | ## Data
102 |
103 | You can POST data using pipes:
104 |
105 | ```bash
106 | > echo "Hello, World!" | rh httpbin.org/post
107 | ```
108 |
109 | You can POST JSON (JSON is the default format):
110 |
111 | ```bash
112 | > rh https://httpbin.org/anything key1=1
113 | ```
114 |
115 | You can POST data using the URL encoded format:
116 |
117 | ```bash
118 | > rh https://httpbin.org/anything key1=1 --form
119 | ```
120 |
121 | Or using the raw flag:
122 |
123 | ```bash
124 | > rh https://httpbin.org/anything --raw='{"key1":1}' Content-Type:application/json
125 | ```
126 |
127 | Or just plain text:
128 |
129 | ```bash
130 | > rh https://httpbin.org/anything --raw=hello
131 | ```
132 |
133 | Or multi-lines:
134 |
135 | ```bash
136 | > rh https://httpbin.org/anything --raw='
137 | {
138 | "inner-planets": ["Mercury", "Venus", "Earth", "Mars"],
139 | "sun": {
140 | "temp": 5778,
141 | "bigger-than-earth": true
142 | }
143 | }
144 | '
145 | ```
146 |
147 | ## Files
148 |
149 | You can download a file and save it:
150 |
151 | ```bash
152 | > rh https://httpbin.org/image/jpeg > image.jpeg
153 | ```
154 |
155 | If you love ```cat``` 🐱, you can upload a file:
156 |
157 | ```bash
158 | > cat info.txt | rh httpbin.org/post
159 | ```
160 |
161 | The following commmand is not available yet, you can upload a file using the symbol ```@``` and the path:
162 |
163 | ```bash
164 | > rh httpbin.org/post @info.txt
165 | ```
166 |
167 | ## More or Less
168 |
169 | If the response is output to another program there is no colours:
170 |
171 | ```bash
172 | > rh :9200/_nodes | more
173 | ```
174 |
175 | But you can preserve the colors with the ```--pretty=color``` option and ```less -R```:
176 |
177 | ```bash
178 | > rh :9200/_nodes --pretty=color | less -R
179 | ```
180 |
181 | ## SSL Certificates
182 |
183 | You can use self-signed certificates (you can use PEM or DER format):
184 |
185 | ```bash
186 | > rh https://localhost:8080 -v --cafile=rsa/ca.cert
187 | ```
188 |
189 | The .der extension is required for using the DER format:
190 |
191 | ```bash
192 | > rh https://localhost:8080 -v --cafile=rsa/ca.der
193 | ```
194 |
195 | ## Some options
196 |
197 | Show the URL and method:
198 |
199 | ```bash
200 | > rh httpbin.org/get -U
201 | ```
202 |
203 | Show the headers (request and response):
204 |
205 | ```bash
206 | > rh httpbin.org/get -hH
207 | ```
208 |
209 | Show the URL, method, headers and the response body as a compact form:
210 |
211 | ```bash
212 | > rh httpbin.org/get -UhHc
213 | ```
214 |
215 | More options:
216 |
217 | ```bash
218 | > rh --help
219 | ```
220 |
--------------------------------------------------------------------------------
/doc/ginger-128.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twigly/rust-http-cli/40555709978446431bd736019a52eb616854fcec/doc/ginger-128.jpeg
--------------------------------------------------------------------------------
/doc/help-and-version.md:
--------------------------------------------------------------------------------
1 | # Note
2 |
3 | Not available yet, work in progress.
4 |
5 | # Help
6 |
7 | - ```rh --help``` to show detailed help
8 | - ```rh -h``` to show a short help
9 |
10 | # Help
11 |
12 | - ```rh --version``` to show detailed version (user agent and version)
13 | - ```rh -v``` to show only the version
14 |
--------------------------------------------------------------------------------
/doc/install.md:
--------------------------------------------------------------------------------
1 | # Install
2 |
3 | ## Cargo
4 |
5 | ```bash
6 | > cargo install rh
7 | ```
8 |
9 | ## MacOS
10 |
11 | Not available yet.
12 |
13 | ```bash
14 | > brew install rh
15 | ```
16 |
17 | ## Ubuntu
18 |
19 | Not available yet.
20 |
21 | ```bash
22 | > sudo apt install rh
23 | ```
24 |
25 | ## More platforms
26 |
27 | Help welcome ;-)
28 |
--------------------------------------------------------------------------------
/doc/json.md:
--------------------------------------------------------------------------------
1 | # JSON
2 |
3 | Items are converted to JSON by default.
4 |
5 | ## Key/value
6 |
7 | Items are a list of key/value. Each key/value is specified as ```key=value```.
8 |
9 | ## Number and string
10 |
11 | If you want to force a number to be as a string, you can use ```/=``` instead of ```=```
12 |
13 | ```bash
14 | > rh httpbin.org/post number1=123 number2/=456 text=hello
15 | ```
16 |
17 | The JSON object will be:
18 |
19 | ```json
20 | {
21 | "number1": 123,
22 | "number2": "456",
23 | "text": "hello",
24 | }
25 | ```
26 |
27 | ## Boolean and string
28 |
29 | If you want to force a boolean to be as a string, you can use ```/=``` instead of ```=```
30 |
31 | ```bash
32 | > rh httpbin.org/post b1=true b2=false b3=y b4=n b5/=true b6/=false b7/=y b8/=n
33 | ```
34 |
35 | The JSON object will be:
36 |
37 | ```json
38 | {
39 | "b1": true,
40 | "b2": false,
41 | "b3": true,
42 | "b4": false,
43 | "b5": "true",
44 | "b6": "false",
45 | "b7": "y",
46 | "b8": "n",
47 | }
48 | ```
--------------------------------------------------------------------------------
/doc/todo.md:
--------------------------------------------------------------------------------
1 | # Tasks to do
2 |
3 | You're welcome to help on any of the following tasks for example.
4 |
5 | Everything that improves performance is very welcome.
6 |
7 | ## Package manager
8 |
9 | - [ ] Homebrew (Mac)
10 | - [ ] MacPorts (Mac)
11 | - [ ] Debian (Linux)
12 | - [ ] Fedora (Linux)
13 | - [ ] Ubuntu (Linux)
14 | - [ ] Alpine (Linux)
15 | - [ ] Arch (Linux)
16 | - [ ] nixOS (Linux)
17 | - [ ] openSUSE (Linux)
18 | - [ ] Void Linux (Linux)
19 | - [ ] Gentoo (Linux)
20 | - [ ] Android
21 | - [ ] Chocolatey (Windows)
22 | - [ ] Others...
23 |
24 | ## Benchmark
25 |
26 | - [ ] Comparison between ```cURL``` and ```rh```
27 |
28 | ## Features
29 |
30 | ### Authentication / proxy
31 |
32 | - [ ] Deal with authentications [see authentication](authentication.md)
33 | - [ ] Deal with proxies
34 |
35 | ### Items / headers
36 |
37 | - [ ] Recognise arrays in data items (ex: ```array=item1,item2,item3```)
38 | - [ ] Recognise files in data items (ex: ```file_content=@/path/file```)
39 | - [ ] Remove headers with ```key:``` and set an empty value with ```"key: "```
40 | - [ ] Read file content using the symbol ```@``` (for example ```--raw=@/path/file``` or ```key=@/path/file```)
41 | - [ ] Append URL parameters via items
42 | - [ ] Option to sort header and JSON keys (for example ```--sort``` to sort both of them, ```--sort=h``` to sort headers, ```--sort=j``` to sort JSON keys)
43 |
44 | ### Content encoding
45 |
46 | - [ ] Read ```content-encoding=gzip``` (https://httpbin.org/gzip)
47 | - [ ] Read ```content-encoding=brotli``` (https://httpbin.org/brotli)
48 | - [ ] Read ```content-encoding=deflate``` (https://httpbin.org/deflate)
49 |
50 | ### Timeout / redirect
51 |
52 | - [ ] Set a max redirects
53 | - [ ] Set a timeout
54 | - [ ] Show redirects if --verbose
55 |
56 | ### Misc
57 |
58 | - [ ] Multi URLs
59 | - [ ] Add an option ```--pretty=format``` to format without colouring
60 | - [ ] Specify cookies without using the ```cookies``` header (and avoid using ```"``` to escape the ```;``` separator) - maybe not worth (low priority)
61 | - [ ] Completion on available platforms
62 |
63 | ## Performance
64 |
65 | - [ ] ```rh``` performance is very good but it would be nice to review the code and try to optimise
66 | - [ ] the current binary size is acceptable but there are certainly ways to decrease it (without sacrificing performance)
67 |
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | max_width = 180
--------------------------------------------------------------------------------
/snapcraft.yaml:
--------------------------------------------------------------------------------
1 | name: rust-http-cli
2 | version: git
3 | summary: A user-friendly command-line tool to request HTTP APis
4 | description: |
5 | Rust HTTP Cli (`rh`) is a user-friendly, lightweight and performant command-line tool to request HTTP APis.
6 | You can debug, test and verify any HTTP APi with rh in a simple and efficient way.
7 | `rh` is focused on performance and stability.
8 | You don't need OpenSSL because `rh` is based on Rustls, a modern TLS library alternative to OpenSSL.
9 |
10 | `rh` is a standalone application with no runtime or garbage collector, so it doesn't require Python or Java installed on your machine for example.
11 | `rh` is based on Rust that is a blazing fast and memory-efficient language.
12 | license: MIT
13 |
14 | base: core18
15 | confinement: strict
16 | grade: stable
17 | compression: lzo
18 |
19 | parts:
20 | rust-http-cli:
21 | plugin: rust
22 | source: https://github.com/twigly/rust-http-cli.git
23 |
24 | apps:
25 | rust-http-cli:
26 | command: bin/rh
27 |
--------------------------------------------------------------------------------