use std::cmp::min;
use std::path::PathBuf;
use std::time::Duration;
use hurl_core::ast::SourceInfo;
use hurl_core::reader::Pos;
use hurl_core::types::Index;
use crate::http::{Call, CookieStore, CurlCmd};
use crate::pretty;
use crate::pretty::PrettyMode;
use crate::pretty::json::Color;
use crate::util::term::Stdout;
use super::error::{RunnerError, RunnerErrorKind};
use super::output::Output;
use super::value::Value;
use super::variable::VariableSet;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct HurlResult {
pub entries: Vec<EntryResult>,
pub duration: Duration,
pub success: bool,
pub cookie_store: CookieStore,
pub timestamp: i64,
pub variables: VariableSet,
}
impl HurlResult {
pub fn errors(&self) -> Vec<(&RunnerError, SourceInfo)> {
let mut errors = vec![];
let mut next_entries = self.entries.iter().skip(1);
for entry in self.entries.iter() {
match next_entries.next() {
None => {
let new_errors = entry.errors.iter().map(|error| (error, entry.source_info));
errors.extend(new_errors);
}
Some(next) => {
if next.entry_index != entry.entry_index {
let new_errors =
entry.errors.iter().map(|error| (error, entry.source_info));
errors.extend(new_errors);
}
}
}
}
errors
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EntryResult {
pub entry_index: Index,
pub source_info: SourceInfo,
pub calls: Vec<Call>,
pub captures: Vec<CaptureResult>,
pub asserts: Vec<AssertResult>,
pub errors: Vec<RunnerError>,
pub transfer_duration: Duration,
pub compressed: bool,
pub curl_cmd: CurlCmd,
}
impl Default for EntryResult {
fn default() -> Self {
EntryResult {
entry_index: Index::new(1),
source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 1)),
calls: vec![],
captures: vec![],
asserts: vec![],
errors: vec![],
transfer_duration: Duration::from_millis(0),
compressed: false,
curl_cmd: CurlCmd::default(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AssertResult {
ImplicitVersion {
actual: String,
expected: String,
source_info: SourceInfo,
},
ImplicitStatus {
actual: u64,
expected: u64,
source_info: SourceInfo,
},
ImplicitHeader {
actual: Result<String, RunnerError>,
expected: String,
source_info: SourceInfo,
},
ImplicitBody {
actual: Result<Value, RunnerError>,
expected: Result<Value, RunnerError>,
source_info: SourceInfo,
},
Explicit {
actual: Result<Option<Value>, RunnerError>,
source_info: SourceInfo,
predicate_result: Option<Result<(), RunnerError>>,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CaptureResult {
pub name: String,
pub value: Value,
}
impl EntryResult {
#[allow(clippy::too_many_arguments)]
pub fn write_response(
&self,
output: Option<&Output>,
stdout: &mut Stdout,
include_headers: bool,
color: bool,
pretty: PrettyMode,
append: bool,
source_info: SourceInfo,
) -> Result<(), RunnerError> {
let Some(call) = self.calls.last() else {
return Ok(());
};
let response = &call.response;
let mut out = Vec::new();
if include_headers {
let text = response.get_status_line_headers(color);
out.append(&mut text.into_bytes());
out.push(b'\n');
}
let body = if self.compressed {
&response
.uncompress_body()
.map_err(|e| RunnerError::new(source_info, RunnerErrorKind::Http(e), false))?
} else {
&response.body
};
let pretty = match pretty {
PrettyMode::Automatic => response.is_json(),
PrettyMode::Force => true,
PrettyMode::None => false,
};
if pretty {
let color_pretty = if color { Color::Ansi } else { Color::NoColor };
let before_len = out.len();
if pretty::format(body, color_pretty, &mut out).is_err() {
out.truncate(before_len);
out.extend_from_slice(body);
}
} else {
out.extend_from_slice(body);
}
if output.is_none() && stdout.is_terminal() && is_binary(&out) {
return Err(RunnerError::new(
source_info,
RunnerErrorKind::BinaryOutput,
false,
));
}
let output = output.unwrap_or(&Output::Stdout);
output.write(&out, stdout, append).map_err(|e| {
let kind = RunnerErrorKind::FileWriteAccess {
path: PathBuf::from(output.to_string()),
error: e.to_string(),
};
RunnerError::new(source_info, kind, false)
})?;
Ok(())
}
}
fn is_binary(bytes: &[u8]) -> bool {
let len = min(2000, bytes.len());
bytes[..len].contains(&0)
}