flq 1.0.0

fetch the web as lean, token-efficient text
Documentation
use std::fs;
use std::io::{self, Read};
use std::time::Duration;

#[derive(Debug)]
pub struct Response {
	pub body: String,
	pub status: u16,
	pub headers: Vec<(String, String)>,
}

pub fn get(url: &str, opts: &crate::Opts) -> Result<Response, String> {
	if url == "-" {
		return Ok(Response {
			body: read_stdin()?,
			status: 200,
			headers: Vec::new(),
		});
	}
	if !url.starts_with("http://") && !url.starts_with("https://") {
		let body = fs::read_to_string(url).map_err(|e| format!("{url}: {e}"))?;
		return Ok(Response {
			body,
			status: 200,
			headers: Vec::new(),
		});
	}

	let ag = ureq::AgentBuilder::new()
		.timeout(Duration::from_secs(opts.max_time))
		.user_agent(&opts.agent)
		.redirects(8)
		.build();

	let mut req = ag.get(url);
	for h in &opts.headers {
		let (k, v) = h
			.split_once(':')
			.ok_or_else(|| format!("bad header: {h}"))?;
		req = req.set(k.trim(), v.trim());
	}
	if let Some(c) = &opts.cookie {
		req = req.set("Cookie", c);
	}

	let res = req.call().map_err(|e| format!("{url}: {e}"))?;
	let status = res.status();
	let headers = res
		.headers_names()
		.iter()
		.filter_map(|k| res.header(k).map(|v| (k.clone(), v.to_string())))
		.collect();
	let body = res
		.into_string()
		.map_err(|e| format!("{url}: read body: {e}"))?;

	Ok(Response {
		body,
		status,
		headers,
	})
}

pub fn read_stdin() -> Result<String, String> {
	let mut buf = String::new();
	io::stdin()
		.read_to_string(&mut buf)
		.map_err(|e| format!("stdin: {e}"))?;
	Ok(buf)
}

#[cfg(test)]
mod tests {
	use super::*;
	use crate::Opts;

	#[test]
	fn reads_local_file() {
		let p = std::env::temp_dir().join("flq-fetch-ok.html");
		fs::write(&p, "<html><body>local</body></html>").unwrap();

		let opts = Opts::default();
		let res = get(p.to_str().unwrap(), &opts).unwrap();
		assert_eq!(res.status, 200);
		assert!(res.headers.is_empty());
		assert!(res.body.contains("local"));

		let _ = fs::remove_file(&p);
	}

	#[test]
	fn missing_local_file_errors() {
		let opts = Opts::default();
		let err = get("this/does/not/exist.html", &opts).unwrap_err();
		assert!(err.contains("this/does/not/exist.html"), "err was: {err}");
	}
}