use crate::error::{FerriError, Result};
use crate::route::{FulfillResponse, RouteHandler};
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HarNotFound {
#[default]
Abort,
Fallback,
}
#[derive(Debug, Clone, Default)]
pub struct RouteFromHarOptions {
pub url: Option<crate::url_matcher::UrlMatcher>,
pub not_found: HarNotFound,
}
#[derive(serde::Deserialize)]
struct HarFile {
log: HarLog,
}
#[derive(serde::Deserialize)]
struct HarLog {
#[serde(default)]
entries: Vec<HarEntry>,
}
#[derive(serde::Deserialize)]
struct HarEntry {
request: HarRequest,
response: HarResponse,
}
#[derive(serde::Deserialize)]
struct HarRequest {
method: String,
url: String,
}
#[derive(serde::Deserialize)]
struct HarResponse {
status: i32,
#[serde(default)]
headers: Vec<HarHeader>,
#[serde(default)]
content: HarContent,
}
#[derive(serde::Deserialize)]
struct HarHeader {
name: String,
value: String,
}
#[derive(serde::Deserialize, Default)]
struct HarContent {
#[serde(default)]
text: Option<String>,
#[serde(default)]
encoding: Option<String>,
#[serde(default, rename = "mimeType")]
mime_type: Option<String>,
}
struct Recorded {
method: String,
url: String,
response: FulfillResponse,
}
impl HarEntry {
fn into_recorded(self) -> Recorded {
let body = match (&self.response.content.text, self.response.content.encoding.as_deref()) {
(Some(text), Some("base64")) => {
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(text)
.unwrap_or_else(|_| text.clone().into_bytes())
},
(Some(text), _) => text.clone().into_bytes(),
(None, _) => Vec::new(),
};
let headers = self
.response
.headers
.into_iter()
.filter(|h| {
let n = h.name.to_ascii_lowercase();
n != "content-length" && n != "content-encoding"
})
.map(|h| (h.name, h.value))
.collect();
Recorded {
method: self.request.method.to_ascii_uppercase(),
url: self.request.url,
response: FulfillResponse {
status: self.response.status,
headers,
body,
content_type: self.response.content.mime_type,
},
}
}
}
pub fn route_handler_from_file(path: &std::path::Path, not_found: HarNotFound) -> Result<RouteHandler> {
let bytes = std::fs::read(path).map_err(|e| FerriError::backend(format!("read HAR {}: {e}", path.display())))?;
let parsed: HarFile =
serde_json::from_slice(&bytes).map_err(|e| FerriError::backend(format!("parse HAR {}: {e}", path.display())))?;
let entries: Arc<Vec<Recorded>> = Arc::new(parsed.log.entries.into_iter().map(HarEntry::into_recorded).collect());
Ok(Arc::new(move |route| {
let req = route.request();
let method = req.method.to_ascii_uppercase();
let url = req.url.clone();
let hit = entries
.iter()
.find(|e| e.method == method && e.url == url)
.or_else(|| entries.iter().find(|e| e.url == url));
match hit {
Some(rec) => route.fulfill(rec.response.clone()),
None => match not_found {
HarNotFound::Abort => route.abort("failed"),
HarNotFound::Fallback => route.fallback(crate::route::ContinueOverrides::default()),
},
}
}))
}