use std::thread;
use std::time::Instant;
use chrono::Utc;
use hurl_core::ast::{Entry, OptionKind, SourceInfo};
use hurl_core::error::{DisplaySourceError, OutputFormat};
use hurl_core::input::Input;
use hurl_core::parser;
use hurl_core::types::{Count, Index};
use crate::http::{Call, Client, CredentialForwarding, FollowLocation};
use crate::util::logger::{ErrorFormat, Logger, LoggerOptions};
use crate::util::term::{Stderr, Stdout, WriteMode};
use super::event::EventListener;
use super::options;
use super::result::{EntryResult, HurlResult};
use super::runner_options::RunnerOptions;
use super::variable::VariableSet;
use super::{Output, entry};
pub fn run(
content: &str,
filename: Option<&Input>,
runner_options: &RunnerOptions,
variables: &VariableSet,
logger_options: &LoggerOptions,
) -> Result<HurlResult, String> {
let mut stdout = Stdout::new(WriteMode::Immediate);
let stderr = Stderr::new(WriteMode::Immediate);
let secrets = variables.secrets();
let mut logger = Logger::new(logger_options, stderr, &secrets);
let hurl_file = parser::parse_hurl_file(content);
let hurl_file = match hurl_file {
Ok(h) => h,
Err(error) => {
let filename = filename.map_or(String::new(), |f| f.to_string());
let message = error.render(
&filename,
content,
None,
OutputFormat::Terminal(logger.color),
);
logger.error_rich(&message);
return Err(error.description());
}
};
let result = run_entries(
&hurl_file.entries,
content,
filename,
runner_options,
variables,
&mut stdout,
None,
&mut logger,
);
if result.success && result.entries.last().is_none() {
let filename = filename.map_or(String::new(), |f| f.to_string());
logger.warning(&format!("No entry have been executed for file {filename}"));
}
Ok(result)
}
#[allow(clippy::too_many_arguments)]
pub fn run_entries(
entries: &[Entry],
content: &str,
filename: Option<&Input>,
runner_options: &RunnerOptions,
variables: &VariableSet,
stdout: &mut Stdout,
listener: Option<&dyn EventListener>,
logger: &mut Logger,
) -> HurlResult {
if entries.is_empty() {
return HurlResult {
success: true,
variables: variables.clone(),
..Default::default()
};
}
let mut http_client = Client::new();
let mut entries_result = vec![];
let mut variables = variables.clone();
let mut current = Index::new(runner_options.from_entry.unwrap_or(1));
let mut repeat_count = 0;
let last = Index::new(runner_options.to_entry.unwrap_or(entries.len()));
let default_verbosity = logger.verbosity;
let start = Instant::now();
let timestamp = Utc::now().timestamp();
log_run_info(entries, runner_options, &variables, logger);
loop {
if current > last {
break;
}
let entry = &entries[current.to_zero_based()];
logger.verbosity = default_verbosity;
let entry_verbosity = options::get_entry_verbosity(entry, default_verbosity, &variables);
if let Ok(entry_verbosity) = entry_verbosity {
logger.verbosity = entry_verbosity;
}
log_run_entry(current, logger);
if let Some(listener) = listener {
listener.on_entry_running(current, last, 0);
}
let options = options::get_entry_options(entry, runner_options, &mut variables, logger);
if let Err(error) = &options {
let entry_result = EntryResult {
entry_index: current,
source_info: entry.source_info(),
errors: vec![error.clone()],
..Default::default()
};
log_errors(&entry_result, content, filename, false, logger);
entries_result.push(entry_result);
if runner_options.continue_on_error {
current += 1;
continue;
} else {
break;
}
}
let options = options.unwrap();
if options.skip {
logger.debug("");
logger.debug_important(&format!("Entry {current} has been skipped"));
current += 1;
continue;
}
if options.repeat == Some(Count::Finite(0)) {
logger.debug("");
logger.debug_important(&format!("Entry {current} is skipped (repeat 0 times)"));
current += 1;
continue;
}
let delay = options.delay;
let delay_ms = delay.as_millis();
if delay_ms > 0 {
logger.debug("");
logger.debug_important(&format!("Delay entry {current} (pause {delay_ms} ms)"));
thread::sleep(delay);
};
let results = run_request(
entry,
current,
last,
content,
filename,
&mut http_client,
&options,
&mut variables,
stdout,
listener,
logger,
);
let has_error = results.last().is_some_and(|r| !r.errors.is_empty());
entries_result.extend(results);
if !runner_options.continue_on_error && has_error {
break;
}
repeat_count += 1;
match options.repeat {
None => {
repeat_count = 0;
current += 1;
}
Some(Count::Finite(n)) => {
if repeat_count >= n {
repeat_count = 0;
current += 1;
} else {
logger
.debug_important(&format!("Repeat entry {current} (x{repeat_count}/{n})"));
}
}
Some(Count::Infinite) => {
logger.debug_important(&format!("Repeat entry {current} (x{repeat_count})"));
}
}
}
let duration = start.elapsed();
let cookie_store = http_client.cookie_store(logger);
let success = is_success(&entries_result);
HurlResult {
entries: entries_result,
duration,
success,
cookie_store,
timestamp,
variables,
}
}
#[allow(clippy::too_many_arguments)]
fn run_request(
entry: &Entry,
current: Index,
last: Index,
content: &str,
filename: Option<&Input>,
http_client: &mut Client,
options: &RunnerOptions,
variables: &mut VariableSet,
stdout: &mut Stdout,
listener: Option<&dyn EventListener>,
logger: &mut Logger,
) -> Vec<EntryResult> {
let mut results = vec![];
let mut retry_count = 0;
loop {
let mut result = entry::run(entry, current, http_client, variables, options, logger);
let has_error = !result.errors.is_empty();
let retry_max_reached = if let Some(Count::Finite(r)) = options.retry {
retry_count >= r
} else {
false
};
if retry_max_reached {
logger.debug_important("Retry max count reached, no more retry");
logger.debug("");
}
let retry = options.retry.is_some() && !retry_max_reached && has_error;
if !has_error && let Some(output) = &options.output {
write_entry_response(
entry,
&mut result,
content,
filename,
output,
options,
stdout,
logger,
);
} else if has_error {
log_errors(&result, content, filename, retry, logger);
}
results.push(result);
if !retry {
break;
}
retry_count += 1;
let delay = options.retry_interval.as_millis();
let retry_max = match options.retry.unwrap() {
Count::Finite(max) => max.to_string(),
Count::Infinite => "ꝏ".to_string(),
};
logger.debug("");
logger.debug_important(&format!(
"Retry on entry {current} (count: {retry_count}/{retry_max}, interval: {delay} ms)"
));
if let Some(listener) = listener {
listener.on_entry_running(current, last, retry_count);
}
thread::sleep(options.retry_interval);
log_run_entry(current, logger);
}
results
}
#[allow(clippy::too_many_arguments)]
fn write_entry_response(
entry: &Entry,
entry_result: &mut EntryResult,
content: &str,
input: Option<&Input>,
output: &Output,
options: &RunnerOptions,
stdout: &mut Stdout,
logger: &mut Logger,
) {
let context_dir = &options.context_dir;
let source_info = get_output_source_info(entry);
let output = match output.clone().try_with(context_dir, source_info) {
Ok(o) => o,
Err(error) => {
let filename = input.map_or(String::new(), |f| f.to_string());
let message = error.render(
&filename,
content,
Some(entry_result.source_info),
OutputFormat::Terminal(logger.color),
);
logger.error_rich(&message);
entry_result.errors.push(error);
return;
}
};
let include_headers = false;
let color = options.color_stdout;
let pretty = options.pretty;
let append = false;
if let Err(error) = entry_result.write_response(
Some(&output),
stdout,
include_headers,
color,
pretty,
append,
source_info,
) {
let filename = input.map_or(String::new(), |f| f.to_string());
let message = error.render(
&filename,
content,
Some(entry_result.source_info),
OutputFormat::Terminal(logger.color),
);
logger.error_rich(&message);
entry_result.errors.push(error);
}
}
fn get_output_source_info(entry: &Entry) -> SourceInfo {
let mut source_info = entry.source_info();
for option_entry in entry.request.options() {
if let OptionKind::Output(value) = &option_entry.kind {
source_info = value.source_info;
}
}
source_info
}
fn is_success(entries: &[EntryResult]) -> bool {
let mut next_entries = entries.iter().skip(1);
for entry in entries.iter() {
match next_entries.next() {
None => return entry.errors.is_empty(),
Some(next) => {
if next.entry_index != entry.entry_index && !entry.errors.is_empty() {
return false;
}
}
}
}
true
}
fn get_non_default_options(options: &RunnerOptions) -> Vec<(&'static str, String)> {
let default_options = RunnerOptions::default();
let mut non_default_options = vec![];
if options.continue_on_error != default_options.continue_on_error {
non_default_options.push(("continue_on_error", options.continue_on_error.to_string()));
}
if options.delay != default_options.delay {
non_default_options.push(("delay", format!("{}ms", options.delay.as_millis() as u64)));
}
if options.follow_location != default_options.follow_location {
match options.follow_location {
FollowLocation::No => {}
FollowLocation::Follow(CredentialForwarding::OnlyInitialHost) => {
non_default_options.push(("follow redirect", "true".to_string()));
}
FollowLocation::Follow(CredentialForwarding::AllHosts) => {
non_default_options.push(("follow redirect", "true (trusted)".to_string()));
}
}
}
if options.insecure != default_options.insecure {
non_default_options.push(("insecure", options.insecure.to_string()));
}
if options.max_redirect != default_options.max_redirect {
non_default_options.push(("max redirect", options.max_redirect.to_string()));
}
if options.proxy != default_options.proxy
&& let Some(proxy) = &options.proxy
{
non_default_options.push(("proxy", proxy.to_string()));
}
if options.retry != default_options.retry {
let value = match options.retry {
Some(retry) => retry.to_string(),
None => "none".to_string(),
};
non_default_options.push(("retry", value));
}
if options.unix_socket != default_options.unix_socket
&& let Some(unix_socket) = &options.unix_socket
{
non_default_options.push(("unix socket", unix_socket.to_string()));
}
non_default_options
}
fn log_run_info(
entries: &[Entry],
runner_options: &RunnerOptions,
variables: &VariableSet,
logger: &mut Logger,
) {
if logger.verbosity.is_some() {
let non_default_options = get_non_default_options(runner_options);
if !non_default_options.is_empty() {
logger.debug_important("Options:");
for (name, value) in non_default_options.iter() {
logger.debug(&format!(" {name}: {value}"));
}
}
}
let variables = variables
.iter()
.filter(|(_, variable)| !variable.is_secret())
.collect::<Vec<_>>();
if !variables.is_empty() {
logger.debug_important("Variables:");
for (name, variable) in variables.iter() {
logger.debug(&format!(" {name}: {}", variable.value()));
}
}
if let Some(to_entry) = runner_options.to_entry {
logger.debug(&format!("Executing {to_entry}/{} entries", entries.len()));
}
}
fn log_errors(
entry_result: &EntryResult,
content: &str,
filename: Option<&Input>,
retry: bool,
logger: &mut Logger,
) {
if retry {
entry_result.errors.iter().for_each(|error| {
logger.debug_error(content, filename, error, entry_result.source_info);
});
return;
}
if logger.error_format == ErrorFormat::Long
&& let Some(Call { response, .. }) = entry_result.calls.last()
{
logger.info_curl_cmd(&entry_result.curl_cmd.to_string());
logger.info("");
response.log_info_all(logger);
}
entry_result.errors.iter().for_each(|error| {
let filename = filename.map_or(String::new(), |f| f.to_string());
let message = error.render(
&filename,
content,
Some(entry_result.source_info),
OutputFormat::Terminal(logger.color),
);
logger.error_rich(&message);
});
}
fn log_run_entry(entry_index: Index, logger: &mut Logger) {
logger.debug_important(
"------------------------------------------------------------------------------",
);
logger.debug_important(&format!("Executing entry {entry_index}"));
}
#[cfg(test)]
mod test {
use super::*;
use crate::runner::RunnerOptionsBuilder;
#[test]
fn get_non_default_options_returns_empty_when_default() {
let options = RunnerOptions::default();
assert!(get_non_default_options(&options).is_empty());
}
#[test]
fn get_non_default_options_returns_only_non_default_options() {
let options = RunnerOptionsBuilder::new()
.delay(std::time::Duration::from_millis(500))
.build();
let non_default_options = get_non_default_options(&options);
assert_eq!(non_default_options.len(), 1);
let first_non_default = non_default_options.first().unwrap();
assert_eq!(first_non_default.0, "delay");
assert_eq!(first_non_default.1, "500ms");
}
}