use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use std::time::Duration;
use hurl::pretty::PrettyMode;
use hurl::runner::Value;
use hurl_core::types::{BytesPerSec, Count, DurationUnit};
use super::context::{
HURL_CONNECT_TIMEOUT, HURL_DELAY, HURL_ERROR_FORMAT, HURL_FOLLOW_LOCATION,
HURL_FOLLOW_LOCATION_TRUSTED, HURL_HEADER, HURL_JOBS, HURL_LIMIT_RATE, HURL_MAX_FILESIZE,
HURL_MAX_REDIRS, HURL_MAX_TIME, HURL_RETRY, HURL_RETRY_INTERVAL, HURL_VERBOSITY,
};
use super::variables::TypeKind;
use super::{
CliOptions, CliOptionsError, ErrorFormat, HttpVersion, IpResolve, OutputType, RunContext,
Verbosity, duration, secret, variables,
};
fn compressed(context: &RunContext, default_value: bool) -> bool {
context.compressed_env_var().unwrap_or(default_value)
}
fn color(context: &RunContext, default_value: bool) -> bool {
if let Some(no_color) = context.no_color_env_var() {
return !no_color;
}
if let Some(color) = context.color_env_var() {
return color;
}
default_value
}
fn connect_timeout(
context: &RunContext,
default_value: Duration,
) -> Result<Duration, CliOptionsError> {
match context.connect_timeout_env_var() {
Some(timeout) => duration::duration_from_str(timeout, DurationUnit::Second)
.map_err(|e| err_from_cli_err(e, HURL_CONNECT_TIMEOUT)),
None => Ok(default_value),
}
}
fn continue_on_error(context: &RunContext, default_value: bool) -> bool {
context.continue_on_error_env_var().unwrap_or(default_value)
}
fn delay(context: &RunContext, default_value: Duration) -> Result<Duration, CliOptionsError> {
match context.delay_env_var() {
Some(delay) => duration::duration_from_str(delay, DurationUnit::MilliSecond)
.map_err(|e| err_from_cli_err(e, HURL_DELAY)),
None => Ok(default_value),
}
}
fn error_format(
context: &RunContext,
default_value: ErrorFormat,
) -> Result<ErrorFormat, CliOptionsError> {
match context.error_format_env_var() {
Some(error_format) => {
ErrorFormat::from_str(error_format).map_err(|e| err_from_cli_err(e, HURL_ERROR_FORMAT))
}
None => Ok(default_value),
}
}
fn follow_location(context: &RunContext, default_value: bool) -> Result<bool, CliOptionsError> {
let value = match (
context.follow_location_env_var(),
context.follow_location_trusted_env_var(),
) {
(Some(true), _) => true,
(Some(false), Some(true)) => {
let error = format!(
"Invalid environment variables configuration {} {}",
HURL_FOLLOW_LOCATION, HURL_FOLLOW_LOCATION_TRUSTED
);
return Err(CliOptionsError::Error(error));
}
(Some(false), _) => false,
(None, Some(true)) => true,
(None, _) => default_value,
};
Ok(value)
}
fn follow_location_trusted(context: &RunContext, default_value: bool) -> bool {
context
.follow_location_trusted_env_var()
.unwrap_or(default_value)
}
fn headers(
context: &RunContext,
default_value: Vec<String>,
) -> Result<Vec<String>, CliOptionsError> {
let mut all_headers = default_value;
if let Some(header) = context.header_env_var() {
let headers = header.split("|").map(|h| h.to_string()).collect::<Vec<_>>();
for h in &headers {
if !h.contains(':') {
let msg = format!("Invalid header <{h}> missing `:`");
return Err(err_from_cli_err(CliOptionsError::Error(msg), HURL_HEADER));
}
}
all_headers.extend(headers);
}
Ok(all_headers)
}
fn http_version(context: &RunContext, default_value: Option<HttpVersion>) -> Option<HttpVersion> {
if let Some(http3) = context.http3_env_var() {
if http3 {
Some(HttpVersion::V3)
} else {
Some(HttpVersion::V2)
}
} else if let Some(http2) = context.http2_env_var() {
if http2 {
Some(HttpVersion::V2)
} else {
Some(HttpVersion::V11)
}
} else if let Some(http11) = context.http11_env_var() {
if http11 {
Some(HttpVersion::V11)
} else {
Some(HttpVersion::V10)
}
} else if let Some(true) = context.http10_env_var() {
Some(HttpVersion::V10)
} else {
default_value
}
}
fn insecure(context: &RunContext, default_value: bool) -> bool {
context.insecure_env_var().unwrap_or(default_value)
}
fn ip_resolve(context: &RunContext, default_value: Option<IpResolve>) -> Option<IpResolve> {
if let Some(ipv6) = context.ipv6_env_var() {
if ipv6 {
Some(IpResolve::IpV6)
} else {
Some(IpResolve::IpV4)
}
} else if let Some(ipv4) = context.ipv4_env_var() {
if ipv4 {
Some(IpResolve::IpV4)
} else {
Some(IpResolve::IpV6)
}
} else {
default_value
}
}
fn jobs(
context: &RunContext,
default_value: Option<usize>,
) -> Result<Option<usize>, CliOptionsError> {
match context.jobs_env_var() {
Some(jobs) => jobs
.parse::<usize>()
.map(Some)
.map_err(|e| err_from(e, HURL_JOBS)),
None => Ok(default_value),
}
}
fn limit_rate(
context: &RunContext,
default_value: Option<BytesPerSec>,
) -> Result<Option<BytesPerSec>, CliOptionsError> {
match context.limit_rate_env_var() {
Some(limit_rate) => limit_rate
.parse::<u64>()
.map(BytesPerSec)
.map(Some)
.map_err(|e| err_from(e, HURL_LIMIT_RATE)),
None => Ok(default_value),
}
}
fn max_filesize(
context: &RunContext,
default_value: Option<u64>,
) -> Result<Option<u64>, CliOptionsError> {
match context.max_filesize_env_var() {
Some(max_filesize) => max_filesize
.parse::<u64>()
.map(Some)
.map_err(|e| err_from(e, HURL_MAX_FILESIZE)),
None => Ok(default_value),
}
}
fn max_redirect(context: &RunContext, default_value: Count) -> Result<Count, CliOptionsError> {
match context.max_redirs_env_var() {
Some(max_redirs) => max_redirs
.parse::<i32>()
.map_err(|e| err_from(e, HURL_MAX_REDIRS))
.and_then(|n| Count::try_from(n).map_err(|e| err_from(&e, HURL_MAX_REDIRS))),
None => Ok(default_value),
}
}
fn no_assert(context: &RunContext, default_value: bool) -> bool {
context.no_assert_env_var().unwrap_or(default_value)
}
fn no_cookie_store(context: &RunContext, default_value: bool) -> bool {
context.no_cookie_store_env_var().unwrap_or(default_value)
}
fn output_type(context: &RunContext, default_value: OutputType) -> OutputType {
match (context.no_output_env_var(), context.test_env_var()) {
(Some(true), _) => OutputType::NoOutput,
(_, Some(true)) => OutputType::NoOutput,
_ => default_value,
}
}
fn parallel(context: &RunContext, default_value: bool) -> bool {
if let Some(true) = context.test_env_var() {
true
} else {
default_value
}
}
fn pretty(context: &RunContext, default_value: PrettyMode) -> PrettyMode {
if let Some(true) = context.pretty_env_var() {
return PrettyMode::Force;
}
if let Some(true) = context.no_pretty_env_var() {
return PrettyMode::None;
}
default_value
}
fn progress_bar(context: &RunContext, default_value: bool) -> bool {
if let Some(true) = context.test_env_var()
&& context.is_stderr_term()
&& !context.is_ci_env_var()
{
return true;
}
default_value
}
fn retry(
context: &RunContext,
default_value: Option<Count>,
) -> Result<Option<Count>, CliOptionsError> {
match context.retry_env_var() {
Some(retry) => retry
.parse::<i32>()
.map_err(|e| err_from(e, HURL_RETRY))
.and_then(|n| Count::try_from(n).map_err(|e| err_from(&e, HURL_RETRY)))
.map(Some),
None => Ok(default_value),
}
}
fn retry_interval(
context: &RunContext,
default_value: Duration,
) -> Result<Duration, CliOptionsError> {
match context.retry_interval_env_var() {
Some(retry_interval) => {
duration::duration_from_str(retry_interval, DurationUnit::MilliSecond)
.map_err(|e| err_from_cli_err(e, HURL_RETRY_INTERVAL))
}
None => Ok(default_value),
}
}
fn test(context: &RunContext, default_value: bool) -> bool {
context.test_env_var().unwrap_or(default_value)
}
fn timeout(context: &RunContext, default_value: Duration) -> Result<Duration, CliOptionsError> {
match context.max_time_env_var() {
Some(max_time) => duration::duration_from_str(max_time, DurationUnit::Second)
.map_err(|e| err_from_cli_err(e, HURL_MAX_TIME)),
None => Ok(default_value),
}
}
fn user(context: &RunContext, default_value: Option<String>) -> Option<String> {
context
.user_env_var()
.map(|s| s.to_string())
.or(default_value)
}
fn user_agent(context: &RunContext, default_value: Option<String>) -> Option<String> {
context
.user_agent_env_var()
.map(|s| s.to_string())
.or(default_value)
}
fn verbosity(
context: &RunContext,
default_value: Option<Verbosity>,
) -> Result<Option<Verbosity>, CliOptionsError> {
let verbosity = if let Some(true) = context.verbose_env_var() {
Some(Verbosity::Verbose)
} else if let Some(true) = context.very_verbose_env_var() {
Some(Verbosity::Debug)
} else if let Some(verbosity) = context.verbosity_env_var() {
let verbosity =
Verbosity::from_str(verbosity).map_err(|e| err_from_cli_err(e, HURL_VERBOSITY))?;
Some(verbosity)
} else {
default_value
};
Ok(verbosity)
}
pub fn parse_env_vars(
context: &RunContext,
default_options: CliOptions,
) -> Result<CliOptions, CliOptionsError> {
let color_stdout = color(context, default_options.color_stdout);
let color_stderr = color(context, default_options.color_stderr);
let compressed = compressed(context, default_options.compressed);
let connect_timeout = connect_timeout(context, default_options.connect_timeout)?;
let continue_on_error = continue_on_error(context, default_options.continue_on_error);
let delay = delay(context, default_options.delay)?;
let error_format = error_format(context, default_options.error_format)?;
let headers = headers(context, default_options.headers)?;
let http_version = http_version(context, default_options.http_version);
let ip_resolve = ip_resolve(context, default_options.ip_resolve);
let no_assert = no_assert(context, default_options.no_assert);
let no_cookie_store = no_cookie_store(context, default_options.no_cookie_store);
let output_type = output_type(context, default_options.output_type);
let follow_location = follow_location(context, default_options.follow_location)?;
let follow_location_trusted =
follow_location_trusted(context, default_options.follow_location_trusted);
let insecure = insecure(context, default_options.insecure);
let jobs = jobs(context, default_options.jobs)?;
let limit_rate = limit_rate(context, default_options.limit_rate)?;
let max_filesize = max_filesize(context, default_options.max_filesize)?;
let max_redirect = max_redirect(context, default_options.max_redirect)?;
let parallel = parallel(context, default_options.parallel);
let pretty = pretty(context, default_options.pretty);
let progress_bar = progress_bar(context, default_options.progress_bar);
let retry = retry(context, default_options.retry)?;
let retry_interval = retry_interval(context, default_options.retry_interval)?;
let secrets = secrets(context, default_options.secrets)?;
let timeout = timeout(context, default_options.timeout)?;
let user = user(context, default_options.user);
let user_agent = user_agent(context, default_options.user_agent);
let variables = variables(context, default_options.variables)?;
let verbosity = verbosity(context, default_options.verbosity)?;
let test = test(context, default_options.test);
Ok(CliOptions {
color_stdout,
color_stderr,
compressed,
connect_timeout,
continue_on_error,
delay,
error_format,
headers,
http_version,
follow_location,
follow_location_trusted,
insecure,
ip_resolve,
jobs,
limit_rate,
max_filesize,
max_redirect,
no_assert,
no_cookie_store,
output_type,
parallel,
pretty,
progress_bar,
retry,
retry_interval,
secrets,
test,
timeout,
user,
user_agent,
variables,
verbosity,
..default_options
})
}
fn variables(
context: &RunContext,
default_variables: HashMap<String, Value>,
) -> Result<HashMap<String, Value>, CliOptionsError> {
let mut variables = default_variables;
let type_kind = TypeKind::Inferred;
for (env_name, env_value) in context.var_env_vars() {
let value = variables::parse_value(env_value, type_kind)?;
variables.insert(env_name.to_string(), value);
}
Ok(variables)
}
fn secrets(
context: &RunContext,
default_secrets: HashMap<String, String>,
) -> Result<HashMap<String, String>, CliOptionsError> {
let mut secrets = default_secrets;
let type_kind = TypeKind::String;
for (env_name, env_value) in context.secret_env_vars() {
let value = variables::parse_value(env_value, type_kind)?;
secret::add_secret(&mut secrets, env_name.to_string(), value)?;
}
Ok(secrets)
}
fn err_from<E: fmt::Display>(error: E, env: &'static str) -> CliOptionsError {
let message = format!("{error} ({env} environment variable)");
CliOptionsError::Error(message)
}
fn err_from_cli_err(error: CliOptionsError, env: &'static str) -> CliOptionsError {
match error {
CliOptionsError::DisplayHelp(_) => error,
CliOptionsError::DisplayVersion(_) => error,
CliOptionsError::NoInput(_) => error,
CliOptionsError::Error(message) => {
let message = format!("{message} ({env} environment variable)");
CliOptionsError::Error(message)
}
CliOptionsError::InvalidInputFile(_) => error,
}
}
#[cfg(test)]
mod tests {
use super::{CliOptions, RunContext, parse_env_vars};
use hurl::runner::{Number, Value};
use std::collections::HashMap;
#[test]
fn test_options_variables_override_by_env_vars() {
let stdin_term = true;
let stdout_term = true;
let stderr_term = true;
let mut options = CliOptions::default();
let mut variables = HashMap::new();
variables.insert("var1".to_string(), Value::String("zzz".to_string()));
variables.insert("foo".to_string(), Value::String("FOO".to_string()));
options.variables = variables;
let env_vars_override = HashMap::from([
("FOO".to_string(), "xxx".to_string()),
("HURL_VARIABLE_foo".to_string(), "48".to_string()),
("HURL_VARIABLE_bar".to_string(), "BAR".to_string()),
("HURL_baz".to_string(), "abcd".to_string()),
("NOT_A_VARIABLE".to_string(), "bar".to_string()),
]);
let ctx = RunContext::new(env_vars_override, stdin_term, stdout_term, stderr_term);
let updated_options = parse_env_vars(&ctx, options).unwrap();
assert_eq!(updated_options.variables.len(), 3);
assert_eq!(
updated_options.variables["foo"],
Value::Number(Number::Integer(48))
);
assert_eq!(
updated_options.variables["var1"],
Value::String("zzz".to_string())
);
assert_eq!(
updated_options.variables["bar"],
Value::String("BAR".to_string())
);
}
#[test]
fn test_options_secrets_override_by_env_vars() {
let stdin_term = true;
let stdout_term = true;
let stderr_term = true;
let mut options = CliOptions::default();
let mut secrets = HashMap::new();
secrets.insert("secret1".to_string(), "SECRET1".to_string());
options.secrets = secrets;
let env_vars_override = HashMap::from([
("QUX".to_string(), "qux".to_string()),
("HURL_SECRET_secret2".to_string(), "SECRET2".to_string()),
("HURL_VARIABLE_bar".to_string(), "BAR".to_string()),
("HURL_SECRET_secret3".to_string(), "SECRET3".to_string()),
]);
let ctx = RunContext::new(env_vars_override, stdin_term, stdout_term, stderr_term);
let updated_options = parse_env_vars(&ctx, options).unwrap();
assert_eq!(updated_options.variables.len(), 1);
assert_eq!(
updated_options.variables["bar"],
Value::String("BAR".to_string())
);
assert_eq!(updated_options.secrets.len(), 3);
assert_eq!(updated_options.secrets["secret1"], "SECRET1".to_string(),);
assert_eq!(updated_options.secrets["secret2"], "SECRET2".to_string(),);
assert_eq!(updated_options.secrets["secret3"], "SECRET3".to_string(),);
}
}