// Request + RequestBuilder — mirrors NPE Request.java. // // PARITY: add_header silently overwrites instead of appending, per NPE // Request.java:215-221. Callers depend on this. append_header is our // own clean addition for callers we control. use std::collections::BTreeMap; use crate::localization::Localization; pub type Headers = BTreeMap>; #[derive(Clone, Debug, Eq, PartialEq)] pub enum Method { Get, Head, Post, Put, Delete, } impl Method { pub fn as_str(&self) -> &'static str { match self { Method::Get => "GET", Method::Head => "HEAD", Method::Post => "POST", Method::Put => "PUT", Method::Delete => "DELETE", } } } #[derive(Clone, Debug)] pub struct Request { method: Method, url: String, headers: Headers, body: Option>, localization: Option, automatic_localization_header: bool, } impl Request { pub fn get(url: impl Into) -> RequestBuilder { RequestBuilder::new(Method::Get, url) } pub fn head(url: impl Into) -> RequestBuilder { RequestBuilder::new(Method::Head, url) } pub fn post(url: impl Into, body: Vec) -> RequestBuilder { RequestBuilder::new(Method::Post, url).body(Some(body)) } pub fn method(&self) -> &Method { &self.method } pub fn url(&self) -> &str { &self.url } pub fn headers(&self) -> &Headers { &self.headers } pub fn body(&self) -> Option<&[u8]> { self.body.as_deref() } pub fn localization(&self) -> Option<&Localization> { self.localization.as_ref() } pub fn automatic_localization_header(&self) -> bool { self.automatic_localization_header } } #[derive(Clone, Debug)] pub struct RequestBuilder { method: Method, url: String, headers: Headers, body: Option>, localization: Option, automatic_localization_header: bool, } impl RequestBuilder { pub fn new(method: Method, url: impl Into) -> Self { Self { method, url: url.into(), headers: BTreeMap::new(), body: None, localization: None, automatic_localization_header: true, } } /// PARITY with NPE Request.Builder.addHeader: silently overwrites any /// existing values for `name`. Callers downstream of NPE-derived code /// depend on this. For new code prefer [`Self::append_header`]. pub fn add_header(mut self, name: impl Into, value: impl Into) -> Self { let key = lowercase(name.into()); self.headers.insert(key, vec![value.into()]); self } /// Appends a value to `name`, creating the entry if absent. This is the /// behavior NPE's addHeader was intended to have. Use freely in our own /// code; avoid when porting NPE call sites that rely on overwrite. pub fn append_header(mut self, name: impl Into, value: impl Into) -> Self { let key = lowercase(name.into()); self.headers.entry(key).or_default().push(value.into()); self } pub fn headers(mut self, headers: Headers) -> Self { self.headers = headers .into_iter() .map(|(k, v)| (lowercase(k), v)) .collect(); self } pub fn body(mut self, body: Option>) -> Self { self.body = body; self } pub fn localization(mut self, localization: Option) -> Self { self.localization = localization; self } pub fn automatic_localization_header(mut self, on: bool) -> Self { self.automatic_localization_header = on; self } pub fn build(self) -> Request { Request { method: self.method, url: self.url, headers: self.headers, body: self.body, localization: self.localization, automatic_localization_header: self.automatic_localization_header, } } } fn lowercase(s: String) -> String { s.to_ascii_lowercase() } #[cfg(test)] mod tests { use super::*; #[test] fn add_header_overwrites_parity() { let r = Request::get("https://x") .add_header("X-Foo", "first") .add_header("X-Foo", "second") .build(); assert_eq!(r.headers().get("x-foo"), Some(&vec!["second".into()])); } #[test] fn append_header_accumulates() { let r = Request::get("https://x") .append_header("X-Foo", "first") .append_header("X-Foo", "second") .build(); assert_eq!( r.headers().get("x-foo"), Some(&vec!["first".into(), "second".into()]) ); } #[test] fn headers_keys_lowercased() { let r = Request::get("https://x").add_header("Content-Type", "text/plain").build(); assert!(r.headers().contains_key("content-type")); assert!(!r.headers().contains_key("Content-Type")); } }