use hurl_core::ast::{
BooleanOption, CountOption, DurationOption, Entry, NaturalOption, Number as AstNumber,
OptionKind, Placeholder, Template, VariableDefinition, VariableValue, VerbosityOption,
};
use hurl_core::types::{BytesPerSec, Count, DurationUnit};
use crate::http::{CredentialForwarding, FollowLocation, Header, IpResolve, RequestedHttpVersion};
use crate::pretty::PrettyMode;
use crate::util::logger::{Logger, Verbosity};
use super::error::{RunnerError, RunnerErrorKind};
use super::expr;
use super::number::Number;
use super::output::Output;
use super::runner_options::RunnerOptions;
use super::template::eval_template;
use super::value::Value;
use super::variable::VariableSet;
pub fn get_entry_options(
entry: &Entry,
runner_options: &RunnerOptions,
variables: &mut VariableSet,
logger: &mut Logger,
) -> Result<RunnerOptions, RunnerError> {
let runner_options = runner_options.clone();
let mut entry_options = RunnerOptions {
output: None,
..runner_options
};
if entry.request.options().is_empty() {
return Ok(entry_options);
}
logger.debug("");
logger.debug_important("Entry options:");
for option in entry.request.options() {
match &option.kind {
OptionKind::AwsSigV4(value) => {
let value = eval_template(value, variables)?;
entry_options.aws_sigv4 = Some(value);
}
OptionKind::CaCertificate(filename) => {
let value = eval_template(filename, variables)?;
entry_options.cacert_file = Some(value);
}
OptionKind::ClientCert(filename) => {
let value = eval_template(filename, variables)?;
entry_options.client_cert_file = Some(value);
}
OptionKind::ClientKey(filename) => {
let value = eval_template(filename, variables)?;
entry_options.client_key_file = Some(value);
}
OptionKind::Compressed(value) => {
let value = eval_boolean_option(value, variables)?;
entry_options.compressed = value;
}
OptionKind::ConnectTo(value) => {
let value = eval_template(value, variables)?;
entry_options.connects_to.push(value);
}
OptionKind::ConnectTimeout(value) => {
let value = eval_duration_option(value, variables, DurationUnit::MilliSecond)?;
entry_options.connect_timeout = value;
}
OptionKind::Delay(value) => {
let value = eval_duration_option(value, variables, DurationUnit::MilliSecond)?;
entry_options.delay = value;
}
OptionKind::Digest(value) => {
let value = eval_boolean_option(value, variables)?;
entry_options.digest = value;
}
OptionKind::Header(header) => {
let header = eval_header_option(header, variables)?;
entry_options.headers.push(header);
}
OptionKind::Http10(value) => {
let value = eval_boolean_option(value, variables)?;
if value {
entry_options.http_version = RequestedHttpVersion::Http10;
}
entry_options.allow_reuse = false;
}
OptionKind::Http11(value) => {
let value = eval_boolean_option(value, variables)?;
if value {
entry_options.http_version = RequestedHttpVersion::Http11;
} else {
entry_options.http_version = RequestedHttpVersion::Http10;
}
entry_options.allow_reuse = false;
}
OptionKind::Http2(value) => {
let value = eval_boolean_option(value, variables)?;
if value {
entry_options.http_version = RequestedHttpVersion::Http2;
} else {
entry_options.http_version = RequestedHttpVersion::Http11;
}
entry_options.allow_reuse = false;
}
OptionKind::Http3(value) => {
let value = eval_boolean_option(value, variables)?;
if value {
entry_options.http_version = RequestedHttpVersion::Http3;
} else {
entry_options.http_version = RequestedHttpVersion::Http2;
}
entry_options.allow_reuse = false;
}
OptionKind::FollowLocation(value) => {
let value = eval_boolean_option(value, variables)?;
if value {
entry_options.follow_location =
FollowLocation::Follow(CredentialForwarding::OnlyInitialHost);
} else {
entry_options.follow_location = FollowLocation::No;
}
}
OptionKind::FollowLocationTrusted(value) => {
let value = eval_boolean_option(value, variables)?;
if value {
entry_options.follow_location =
FollowLocation::Follow(CredentialForwarding::AllHosts);
}
}
OptionKind::Insecure(value) => {
let value = eval_boolean_option(value, variables)?;
entry_options.insecure = value;
}
OptionKind::IpV4(value) => {
let value = eval_boolean_option(value, variables)?;
entry_options.ip_resolve = if value {
IpResolve::IpV4
} else {
IpResolve::IpV6
}
}
OptionKind::IpV6(value) => {
let value = eval_boolean_option(value, variables)?;
entry_options.ip_resolve = if value {
IpResolve::IpV6
} else {
IpResolve::IpV4
}
}
OptionKind::LimitRate(value) => {
let value = eval_natural_option(value, variables)?;
entry_options.max_send_speed = Some(BytesPerSec(value));
entry_options.max_recv_speed = Some(BytesPerSec(value));
}
OptionKind::MaxRedirect(value) => {
let value = eval_count_option(value, variables)?;
entry_options.max_redirect = value;
}
OptionKind::MaxTime(value) => {
let value = eval_duration_option(value, variables, DurationUnit::MilliSecond)?;
entry_options.timeout = value;
}
OptionKind::Negotiate(value) => {
let value = eval_boolean_option(value, variables)?;
entry_options.negotiate = value;
}
OptionKind::NetRc(value) => {
let value = eval_boolean_option(value, variables)?;
entry_options.netrc = value;
}
OptionKind::NetRcFile(value) => {
let filename = eval_template(value, variables)?;
entry_options.netrc_file = Some(filename);
}
OptionKind::NetRcOptional(value) => {
let value = eval_boolean_option(value, variables)?;
entry_options.netrc_optional = value;
}
OptionKind::Ntlm(value) => {
let value = eval_boolean_option(value, variables)?;
entry_options.ntlm = value;
}
OptionKind::Output(output) => {
let filename = eval_template(output, variables)?;
let output = Output::new(&filename);
if let Output::File(_) = output {
entry_options.pretty = PrettyMode::None;
}
entry_options.output = Some(output);
}
OptionKind::PathAsIs(value) => {
let value = eval_boolean_option(value, variables)?;
entry_options.path_as_is = value;
}
OptionKind::PinnedPublicKey(value) => {
let value = eval_template(value, variables)?;
entry_options.pinned_pub_key = Some(value);
}
OptionKind::Proxy(value) => {
let value = eval_template(value, variables)?;
entry_options.proxy = Some(value);
}
OptionKind::Repeat(value) => {
let value = eval_count_option(value, variables)?;
entry_options.repeat = Some(value);
}
OptionKind::Resolve(value) => {
let value = eval_template(value, variables)?;
entry_options.resolves.push(value);
}
OptionKind::Retry(value) => {
let value = eval_count_option(value, variables)?;
entry_options.retry = Some(value);
}
OptionKind::RetryInterval(value) => {
let value = eval_duration_option(value, variables, DurationUnit::MilliSecond)?;
entry_options.retry_interval = value;
}
OptionKind::Skip(value) => {
let value = eval_boolean_option(value, variables)?;
entry_options.skip = value;
}
OptionKind::UnixSocket(value) => {
let value = eval_template(value, variables)?;
entry_options.unix_socket = Some(value);
}
OptionKind::User(value) => {
let value = eval_template(value, variables)?;
entry_options.user = Some(value);
}
OptionKind::Variable(VariableDefinition { name, value, .. }) => {
let value = eval_variable_value(value, variables)?;
variables.insert(name.clone(), value);
}
OptionKind::Verbosity(_) => {
}
OptionKind::Verbose(value) => {
eval_boolean_option(value, variables)?;
}
OptionKind::VeryVerbose(value) => {
eval_boolean_option(value, variables)?;
}
}
logger.debug(&option.kind.to_string());
}
Ok(entry_options)
}
pub fn get_entry_verbosity(
entry: &Entry,
default_verbosity: Option<Verbosity>,
variables: &VariableSet,
) -> Result<Option<Verbosity>, RunnerError> {
let mut verbosity = default_verbosity;
for option in entry.request.options() {
match &option.kind {
OptionKind::Verbosity(value) => {
verbosity = match value {
VerbosityOption::Brief => Some(Verbosity::LowVerbose),
VerbosityOption::Verbose => Some(Verbosity::Verbose),
VerbosityOption::Debug => Some(Verbosity::VeryVerbose),
}
}
OptionKind::Verbose(value) => {
let value = eval_boolean_option(value, variables)?;
verbosity = if value {
Some(Verbosity::Verbose)
} else {
None
}
}
OptionKind::VeryVerbose(value) => {
let value = eval_boolean_option(value, variables)?;
verbosity = if value {
Some(Verbosity::VeryVerbose)
} else {
None
}
}
_ => {}
}
}
Ok(verbosity)
}
fn eval_header_option(header: &Template, variables: &VariableSet) -> Result<Header, RunnerError> {
let source_info = header.source_info;
let header = eval_template(header, variables)?;
Header::parse(&header).ok_or({
let message = "missing `:`".to_string();
let kind = RunnerErrorKind::InvalidOptionValue {
name: "header".to_string(),
value: header,
message,
};
RunnerError::new(source_info, kind, false)
})
}
fn eval_boolean_option(
boolean_value: &BooleanOption,
variables: &VariableSet,
) -> Result<bool, RunnerError> {
match boolean_value {
BooleanOption::Literal(value) => Ok(*value),
BooleanOption::Placeholder(Placeholder { expr, .. }) => {
match expr::eval(expr, variables)? {
Value::Bool(value) => Ok(value),
v => {
let kind = RunnerErrorKind::ExpressionInvalidType {
value: v.repr(),
expecting: "boolean".to_string(),
};
Err(RunnerError::new(expr.source_info, kind, false))
}
}
}
}
}
fn eval_natural_option(
natural_value: &NaturalOption,
variables: &VariableSet,
) -> Result<u64, RunnerError> {
match natural_value {
NaturalOption::Literal(value) => Ok(value.as_u64()),
NaturalOption::Placeholder(Placeholder { expr, .. }) => {
match expr::eval(expr, variables)? {
Value::Number(Number::Integer(value)) => {
if value > 0 {
Ok(value as u64)
} else {
let kind = RunnerErrorKind::ExpressionInvalidType {
value: format!("integer <{value}>"),
expecting: "integer > 0".to_string(),
};
Err(RunnerError::new(expr.source_info, kind, false))
}
}
v => {
let kind = RunnerErrorKind::ExpressionInvalidType {
value: v.repr(),
expecting: "integer".to_string(),
};
Err(RunnerError::new(expr.source_info, kind, false))
}
}
}
}
}
fn eval_count_option(
count_value: &CountOption,
variables: &VariableSet,
) -> Result<Count, RunnerError> {
match count_value {
CountOption::Literal(repeat) => Ok(*repeat),
CountOption::Placeholder(Placeholder { expr, .. }) => match expr::eval(expr, variables)? {
Value::Number(Number::Integer(value)) => {
if value == -1 {
Ok(Count::Infinite)
} else if value >= 0 {
Ok(Count::Finite(value as usize))
} else {
let kind = RunnerErrorKind::ExpressionInvalidType {
value: format!("integer <{value}>"),
expecting: "integer >= -1".to_string(),
};
Err(RunnerError::new(expr.source_info, kind, false))
}
}
v => {
let kind = RunnerErrorKind::ExpressionInvalidType {
value: v.repr(),
expecting: "integer".to_string(),
};
Err(RunnerError::new(expr.source_info, kind, false))
}
},
}
}
fn eval_duration_option(
duration_value: &DurationOption,
variables: &VariableSet,
default_unit: DurationUnit,
) -> Result<std::time::Duration, RunnerError> {
let millis = match duration_value {
DurationOption::Literal(literal) => {
let unit = literal.unit.unwrap_or(default_unit);
match unit {
DurationUnit::MilliSecond => literal.value.as_u64(),
DurationUnit::Second => literal.value.as_u64() * 1000,
DurationUnit::Minute => literal.value.as_u64() * 1000 * 60,
DurationUnit::Hour => literal.value.as_u64() * 1000 * 60 * 60,
}
}
DurationOption::Placeholder(Placeholder { expr, .. }) => match expr::eval(expr, variables)?
{
Value::Number(Number::Integer(value)) => {
if value < 0 {
let kind = RunnerErrorKind::ExpressionInvalidType {
value: format!("integer <{value}>"),
expecting: "positive integer".to_string(),
};
return Err(RunnerError::new(expr.source_info, kind, false));
} else {
match default_unit {
DurationUnit::MilliSecond => value as u64,
DurationUnit::Second => (value * 1000) as u64,
DurationUnit::Minute => (value * 1000 * 60) as u64,
DurationUnit::Hour => (value * 1000 * 60 * 60) as u64,
}
}
}
v => {
let kind = RunnerErrorKind::ExpressionInvalidType {
value: v.repr(),
expecting: "positive integer".to_string(),
};
return Err(RunnerError::new(expr.source_info, kind, false));
}
},
};
Ok(std::time::Duration::from_millis(millis))
}
fn eval_variable_value(
variable_value: &VariableValue,
variables: &mut VariableSet,
) -> Result<Value, RunnerError> {
match variable_value {
VariableValue::Null => Ok(Value::Null),
VariableValue::Bool(v) => Ok(Value::Bool(*v)),
VariableValue::Number(v) => Ok(eval_number(v)),
VariableValue::String(template) => {
let s = eval_template(template, variables)?;
Ok(Value::String(s))
}
}
}
fn eval_number(number: &AstNumber) -> Value {
match number {
AstNumber::Float(value) => Value::Number(Number::Float(value.as_f64())),
AstNumber::Integer(value) => Value::Number(Number::Integer(value.as_i64())),
AstNumber::BigInteger(value) => Value::Number(Number::BigInteger(value.clone())),
}
}
#[cfg(test)]
mod tests {
use hurl_core::ast::{
Duration, Expr, ExprKind, Placeholder, SourceInfo, U64, Variable, Whitespace,
};
use hurl_core::reader::Pos;
use hurl_core::types::{DurationUnit, ToSource};
use super::*;
use crate::runner::RunnerErrorKind;
fn verbose_option_template() -> BooleanOption {
BooleanOption::Placeholder(Placeholder {
space0: Whitespace {
value: String::new(),
source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
},
expr: Expr {
kind: ExprKind::Variable(Variable {
name: "verbose".to_string(),
source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
}),
source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
},
space1: Whitespace {
value: String::new(),
source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
},
})
}
fn retry_option_template() -> DurationOption {
DurationOption::Placeholder(Placeholder {
space0: Whitespace {
value: String::new(),
source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
},
expr: Expr {
kind: ExprKind::Variable(Variable {
name: "retry".to_string(),
source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
}),
source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
},
space1: Whitespace {
value: String::new(),
source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
},
})
}
#[test]
fn test_eval_boolean_option() {
let mut variables = VariableSet::default();
assert!(eval_boolean_option(&BooleanOption::Literal(true), &variables).unwrap());
variables.insert("verbose".to_string(), Value::Bool(true));
assert!(eval_boolean_option(&verbose_option_template(), &variables).unwrap());
}
#[test]
fn test_eval_boolean_option_error() {
let mut variables = VariableSet::default();
let error = eval_boolean_option(&verbose_option_template(), &variables)
.err()
.unwrap();
assert!(!error.assert);
assert_eq!(
error.kind,
RunnerErrorKind::TemplateVariableNotDefined {
name: "verbose".to_string()
}
);
variables.insert("verbose".to_string(), Value::Number(Number::Integer(10)));
let error = eval_boolean_option(&verbose_option_template(), &variables)
.err()
.unwrap();
assert_eq!(
error.kind,
RunnerErrorKind::ExpressionInvalidType {
value: "integer <10>".to_string(),
expecting: "boolean".to_string()
}
);
}
#[test]
fn test_eval_natural_option() {
let mut variables = VariableSet::default();
assert_eq!(
eval_duration_option(
&DurationOption::Literal(Duration::new(
U64::new(1, "1".to_source()),
Some(DurationUnit::Second)
)),
&variables,
DurationUnit::MilliSecond
)
.unwrap(),
std::time::Duration::from_millis(1000)
);
variables.insert("retry".to_string(), Value::Number(Number::Integer(10)));
assert_eq!(
eval_duration_option(
&retry_option_template(),
&variables,
DurationUnit::MilliSecond
)
.unwrap(),
std::time::Duration::from_millis(10)
);
}
}