#![allow(clippy::needless_pass_by_value)]
use boa_engine::interop::JsClass;
use boa_engine::object::builtins::{JsArray, TypedJsFunction};
use boa_engine::value::{Convert, TryFromJs};
use boa_engine::{
Context, Finalize, JsData, JsObject, JsResult, JsString, JsValue, Trace, boa_class, js_error,
};
use http::header::HeaderMap as HttpHeaderMap;
use http::{HeaderName, HeaderValue};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
use std::str::FromStr;
pub type ForEachCallback = TypedJsFunction<(JsString, JsString, JsObject), ()>;
#[inline]
fn to_header_name(key: impl AsRef<str>) -> JsResult<HeaderName> {
HeaderName::from_str(key.as_ref())
.map_err(|_| js_error!("Cannot convert key to header string as it is not valid ASCII."))
}
#[inline]
fn to_header_value(value: impl AsRef<str>) -> JsResult<HeaderValue> {
value
.as_ref()
.parse()
.map_err(|_| js_error!("Cannot convert value to header string as it is not valid ASCII."))
}
#[derive(Debug, Default, Clone, JsData, Trace, Finalize)]
pub struct JsHeaders {
#[unsafe_ignore_trace]
headers: Rc<RefCell<HttpHeaderMap>>,
}
impl TryFromJs for JsHeaders {
fn try_from_js(value: &JsValue, context: &mut Context) -> JsResult<Self> {
let o = value.to_object(context)?;
let mut this = JsHeaders::default();
for k in &o.own_property_keys(context)? {
let value = o.get(k.clone(), context)?;
this.append(
Convert::from(k.to_string()),
Convert::try_from_js(&value, context)?,
)?;
}
Ok(this)
}
}
impl JsHeaders {
#[must_use]
pub fn from_http(http: HttpHeaderMap) -> Self {
Self {
headers: Rc::new(RefCell::new(http)),
}
}
}
#[boa_class(rename = "Headers")]
#[boa(rename_all = "camelCase")]
impl JsHeaders {
#[boa(constructor)]
fn constructor(init: JsValue, context: &mut Context) -> JsResult<Self> {
let headers = JsHeaders::default();
if init.is_undefined() {
return Ok(headers);
}
let mut h = headers.headers.borrow_mut();
if let Some(other_header) = init
.as_object()
.as_ref()
.and_then(JsObject::downcast_ref::<JsHeaders>)
{
for (key, value) in other_header.headers.borrow().iter() {
if h.contains_key(key) {
h.append(key, value.clone());
} else {
h.insert(key, value.clone());
}
}
} else if let Ok(init) = Vec::<(String, Convert<String>)>::try_from_js(&init, context) {
for (k, v) in init {
let key = to_header_name(k)?;
let value = to_header_value(&v.0)?;
if h.contains_key(&key) {
h.append(key, value);
} else {
h.insert(key, value);
}
}
} else if let Ok(init) = BTreeMap::<String, Convert<String>>::try_from_js(&init, context) {
for (k, v) in init {
let key = to_header_name(k)?;
let value = to_header_value(&v.0)?;
if h.contains_key(&key) {
h.append(key, value);
} else {
h.insert(key, value);
}
}
} else {
return Err(js_error!(TypeError: "Cannot convert init to header object."));
}
drop(h);
Ok(headers)
}
pub fn append(&mut self, key: Convert<String>, value: Convert<String>) -> JsResult<()> {
let key = to_header_name(key.as_ref())?;
let value = to_header_value(value.as_ref())?;
if !self.headers.borrow_mut().append(&key, value.clone()) {
self.headers.borrow_mut().insert(key, value);
}
Ok(())
}
pub fn delete(&mut self, key: Convert<String>) -> JsResult<()> {
let key = to_header_name(key.as_ref())?;
self.headers.borrow_mut().remove(key);
Ok(())
}
pub fn entries(&self, context: &mut Context) -> JsValue {
JsArray::from_iter(
self.headers
.borrow()
.iter()
.map(|(k, v)| {
let k: JsValue = JsString::from(k.as_str()).into();
let v: JsValue = JsString::from(v.to_str().unwrap_or_default()).into();
JsArray::from_iter([k, v], context).into()
})
.collect::<Vec<_>>(),
context,
)
.into()
}
#[allow(clippy::needless_pass_by_value)]
#[boa(method)]
pub fn for_each(
this: JsClass<Self>,
callback: ForEachCallback,
this_arg: Option<JsValue>,
context: &mut Context,
) -> JsResult<()> {
let object = this.inner().upcast();
let this_arg = this_arg.unwrap_or_default();
for (k, v) in this.clone_inner().headers.borrow().iter() {
let k = JsString::from(k.as_str());
let v = JsString::from(v.to_str().unwrap_or(""));
callback.call_with_this(&this_arg, context, (v, k, object.clone()))?;
}
Ok(())
}
pub fn get(&self, key: JsValue, context: &mut Context) -> JsResult<JsValue> {
let key: Convert<String> = Convert::try_from_js(&key, context)?;
let name = to_header_name(key.as_ref())?;
let value = self
.headers
.borrow()
.get_all(name.clone())
.into_iter()
.map(|v| v.to_str().unwrap_or(""))
.fold(None, |mut acc, v| {
let str = acc.get_or_insert_with(String::new);
if !str.is_empty() {
str.push(',');
}
str.push_str(v);
acc
});
Ok(value.map_or_else(JsValue::null, |v| JsString::from(v).into()))
}
fn get_set_cookie(&self) -> Vec<JsString> {
self.headers
.borrow()
.get_all("Set-Cookie")
.into_iter()
.map(|v| JsString::from(v.to_str().unwrap_or("")))
.collect()
}
pub fn has(&self, key: Convert<String>) -> JsResult<bool> {
let key = to_header_name(key.as_ref())?;
Ok(self.headers.borrow().get(key).is_some())
}
#[allow(clippy::unused_self)]
fn keys(&self) -> Vec<JsString> {
self.headers
.borrow()
.keys()
.map(|k| JsString::from(k.as_str()))
.collect()
}
fn set(&mut self, key: Convert<String>, value: Convert<String>) -> JsResult<()> {
let key = to_header_name(key.as_ref())?;
let value = to_header_value(value.as_ref())?;
self.headers.borrow_mut().insert(key, value);
Ok(())
}
fn values(&self) -> Vec<JsString> {
self.headers
.borrow()
.values()
.map(|v| JsString::from(v.to_str().unwrap_or("")))
.collect()
}
}