diff --git a/bitreq/Cargo.toml b/bitreq/Cargo.toml index fa64d0f07..a9206b8cf 100644 --- a/bitreq/Cargo.toml +++ b/bitreq/Cargo.toml @@ -50,6 +50,7 @@ features = ["json-using-serde", "proxy", "https"] default = ["std"] std = [] +forms = ["serde/std", "std"] log = ["dep:log"] json-using-serde = ["serde", "serde_json"] proxy = ["base64", "std"] diff --git a/bitreq/src/error.rs b/bitreq/src/error.rs index 9eb4346d1..80b5e46f8 100644 --- a/bitreq/src/error.rs +++ b/bitreq/src/error.rs @@ -12,6 +12,11 @@ pub enum Error { #[cfg(feature = "json-using-serde")] /// Ran into a Serde error. SerdeJsonError(serde_json::Error), + + #[cfg(feature = "forms")] + /// Ran into a URL encoding error. + SerdeUrlencodeError(crate::urlencode::Error), + /// The response body contains invalid UTF-8, so the `as_str()` /// conversion failed. InvalidUtf8InBody(str::Utf8Error), @@ -95,6 +100,8 @@ impl fmt::Display for Error { match self { #[cfg(feature = "json-using-serde")] SerdeJsonError(err) => write!(f, "{}", err), + #[cfg(feature = "forms")] + SerdeUrlencodeError(err) => write!(f, "{}", err), #[cfg(feature = "std")] IoError(err) => write!(f, "{}", err), InvalidUtf8InBody(err) => write!(f, "{}", err), diff --git a/bitreq/src/lib.rs b/bitreq/src/lib.rs index b8a53bbd6..ff7085f06 100644 --- a/bitreq/src/lib.rs +++ b/bitreq/src/lib.rs @@ -259,6 +259,8 @@ mod http_url; mod proxy; mod request; mod response; +#[cfg(feature = "forms")] +mod urlencode; #[cfg(feature = "async")] pub use client::{Client, RequestExt}; diff --git a/bitreq/src/request.rs b/bitreq/src/request.rs index 7f9b5d530..3ee555d3a 100644 --- a/bitreq/src/request.rs +++ b/bitreq/src/request.rs @@ -159,6 +159,18 @@ impl Request { self.with_header("Content-Length", format!("{}", body_length)) } + /// Add support for form url encode + #[cfg(feature = "forms")] + pub fn with_form(mut self, body: &T) -> Result { + self.headers + .insert("Content-Type".to_string(), "application/x-www-form-urlencoded".to_string()); + + match crate::urlencode::to_string(body) { + Ok(json) => Ok(self.with_body(json)), + Err(err) => Err(Error::SerdeUrlencodeError(err)), + } + } + /// Adds given key and value as query parameter to request url /// (resource). /// @@ -711,3 +723,76 @@ mod encoding_tests { assert_eq!(&req.url.path_and_query, "/?%C3%B3w%C3%B2=what%27s%20this?%20%F0%9F%91%80"); } } + +#[cfg(all(test, feature = "forms"))] +mod form_tests { + use alloc::collections::BTreeMap; + + use super::post; + + #[test] + fn test_with_form_sets_content_type() { + let mut form_data = BTreeMap::new(); + form_data.insert("key", "value"); + + let req = + post("http://www.example.org").with_form(&form_data).expect("form encoding failed"); + assert_eq!( + req.headers.get("Content-Type"), + Some(&"application/x-www-form-urlencoded".to_string()) + ); + } + + #[test] + fn test_with_form_sets_content_length() { + let mut form_data = BTreeMap::new(); + form_data.insert("key", "value"); + + let req = + post("http://www.example.org").with_form(&form_data).expect("form encoding failed"); + // "key=value" is 9 bytes + assert_eq!(req.headers.get("Content-Length"), Some(&"9".to_string())); + } + + #[test] + fn test_with_form_encodes_body() { + let mut form_data = BTreeMap::new(); + form_data.insert("name", "test"); + form_data.insert("value", "42"); + + let req = + post("http://www.example.org").with_form(&form_data).expect("form encoding failed"); + let body = req.body.expect("body should be set"); + let body_str = String::from_utf8(body).expect("body should be valid UTF-8"); + // BTreeMap provides ordered iteration + assert_eq!(body_str, "name=test&value=42"); + } + + #[test] + fn test_with_form_encodes_special_characters() { + let mut form_data = BTreeMap::new(); + form_data.insert("message", "hello world"); + form_data.insert("special", "a&b=c"); + + let req = + post("http://www.example.org").with_form(&form_data).expect("form encoding failed"); + let body = req.body.expect("body should be set"); + let body_str = String::from_utf8(body).expect("body should be valid UTF-8"); + // Spaces are encoded as + and special chars are percent-encoded + assert!( + body_str.contains("message=hello+world") || body_str.contains("message=hello%20world") + ); + assert!(body_str.contains("special=a%26b%3Dc")); + } + + #[test] + fn test_with_form_empty() { + let form_data: BTreeMap<&str, &str> = BTreeMap::new(); + + let req = + post("http://www.example.org").with_form(&form_data).expect("form encoding failed"); + let body = req.body.expect("body should be set"); + assert!(body.is_empty()); + assert_eq!(req.headers.get("Content-Length"), Some(&"0".to_string())); + } +} diff --git a/bitreq/src/urlencode.rs b/bitreq/src/urlencode.rs new file mode 100644 index 000000000..46ce98c9f --- /dev/null +++ b/bitreq/src/urlencode.rs @@ -0,0 +1,940 @@ +use core::fmt; + +use serde::ser::{ + Error as SerError, Impossible, SerializeMap, SerializeSeq, SerializeStruct, SerializeTuple, + Serializer, +}; +use serde::Serialize; + +/// Error type for URL encoding serialization. +#[derive(Debug)] +pub struct Error(String); + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, f) } +} + +impl std::error::Error for Error {} + +impl serde::ser::Error for Error { + fn custom(msg: T) -> Self { Error(msg.to_string()) } +} + +/// Serialize to a URL query string. +pub fn to_string(value: &T) -> Result { + let mut serializer = UrlSerializer { output: String::new() }; + value.serialize(&mut serializer)?; + Ok(serializer.output) +} + +/// Percent-encode a string for use in URL form data. +fn percent_encode(s: &str) -> String { + let mut result = String::with_capacity(s.len()); + for c in s.chars() { + match c { + 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => result.push(c), + ' ' => result.push('+'), + _ => + for byte in c.to_string().as_bytes() { + result.push_str(&format!("%{:02X}", byte)); + }, + } + } + result +} + +struct UrlSerializer { + output: String, +} + +impl UrlSerializer { + fn push_pair(&mut self, key: &str, value: &str) { + if !self.output.is_empty() { + self.output.push('&'); + } + self.output.push_str(key); + self.output.push('='); + self.output.push_str(value); + } +} + +impl<'a> Serializer for &'a mut UrlSerializer { + type Ok = (); + type Error = Error; + + type SerializeStruct = Self; + type SerializeMap = UrlMapSerializer<'a>; + type SerializeSeq = UrlSeqSerializer<'a>; + + type SerializeTuple = Impossible<(), Self::Error>; + type SerializeTupleStruct = Impossible<(), Self::Error>; + type SerializeTupleVariant = Impossible<(), Self::Error>; + type SerializeStructVariant = Impossible<(), Self::Error>; + + // --- Struct (flat) --- + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Ok(self) + } + + // --- Map (top-level HashMap, BTreeMap, etc.) --- + fn serialize_map(self, _len: Option) -> Result { + Ok(UrlMapSerializer { ser: self }) + } + + // --- Seq (top-level Vec<(K,V)>) --- + fn serialize_seq(self, _len: Option) -> Result { + Ok(UrlSeqSerializer { ser: self }) + } + + // We intentionally do NOT support arbitrary scalars as top-level. + fn serialize_str(self, _v: &str) -> Result { + Err(SerError::custom("top-level string not supported; use struct/map/vec of pairs")) + } + fn serialize_bool(self, _v: bool) -> Result { + Err(SerError::custom("top-level scalar not supported; use struct/map/vec of pairs")) + } + fn serialize_i32(self, _v: i32) -> Result { + Err(SerError::custom("top-level scalar not supported; use struct/map/vec of pairs")) + } + fn serialize_u32(self, _v: u32) -> Result { + Err(SerError::custom("top-level scalar not supported; use struct/map/vec of pairs")) + } + + // Everything else: keep it minimal. + fn serialize_none(self) -> Result { + Err(SerError::custom("top-level none not supported")) + } + fn serialize_unit(self) -> Result { + Err(SerError::custom("top-level unit not supported")) + } + + // The rest can be added if you need them later. + fn serialize_i128(self, _v: i128) -> Result { + Err(SerError::custom("top-level scalar not supported; use struct/map/vec of pairs")) + } + fn serialize_u128(self, _v: u128) -> Result { + Err(SerError::custom("top-level scalar not supported; use struct/map/vec of pairs")) + } + + fn serialize_i8(self, _v: i8) -> Result { self.serialize_i32(0) } + fn serialize_i16(self, _v: i16) -> Result { self.serialize_i32(0) } + fn serialize_i64(self, _v: i64) -> Result { self.serialize_i32(0) } + fn serialize_u8(self, _v: u8) -> Result { self.serialize_u32(0) } + fn serialize_u16(self, _v: u16) -> Result { self.serialize_u32(0) } + fn serialize_u64(self, _v: u64) -> Result { self.serialize_u32(0) } + fn serialize_f32(self, _v: f32) -> Result { + Err(SerError::custom("top-level scalar not supported; use struct/map/vec of pairs")) + } + fn serialize_f64(self, _v: f64) -> Result { + Err(SerError::custom("top-level scalar not supported; use struct/map/vec of pairs")) + } + fn serialize_char(self, _v: char) -> Result { + Err(SerError::custom("top-level scalar not supported; use struct/map/vec of pairs")) + } + fn serialize_bytes(self, _v: &[u8]) -> Result { + Err(SerError::custom("bytes not supported")) + } + fn serialize_some(self, _value: &T) -> Result { + Err(SerError::custom("top-level some not supported")) + } + fn serialize_unit_struct(self, _name: &'static str) -> Result { + Err(SerError::custom("unit struct not supported")) + } + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + Err(SerError::custom("unit variant not supported")) + } + fn serialize_newtype_struct( + self, + _name: &'static str, + _value: &T, + ) -> Result { + Err(SerError::custom("newtype struct not supported")) + } + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result { + Err(SerError::custom("newtype variant not supported")) + } + fn serialize_tuple(self, _len: usize) -> Result { + Err(SerError::custom("top-level tuple not supported")) + } + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(SerError::custom("tuple struct not supported")) + } + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(SerError::custom("tuple variant not supported")) + } + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(SerError::custom("struct variant not supported")) + } + + fn collect_str(self, _value: &T) -> Result { + Err(SerError::custom("top-level string not supported; use struct/map/vec of pairs")) + } +} + +// -------------------- struct support -------------------- + +impl<'a> SerializeStruct for &'a mut UrlSerializer { + type Ok = (); + type Error = Error; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> { + let mut vs = ValueSerializer::default(); + value.serialize(&mut vs)?; + self.push_pair(key, &vs.value); + Ok(()) + } + + fn end(self) -> Result { Ok(()) } +} + +// -------------------- map support -------------------- + +struct UrlMapSerializer<'a> { + ser: &'a mut UrlSerializer, +} + +impl<'a> SerializeMap for UrlMapSerializer<'a> { + type Ok = (); + type Error = Error; + + fn serialize_entry( + &mut self, + key: &K, + value: &V, + ) -> Result<(), Self::Error> { + let mut ks = KeySerializer::default(); + key.serialize(&mut ks)?; + + let mut vs = ValueSerializer::default(); + value.serialize(&mut vs)?; + + self.ser.push_pair(&ks.key, &vs.value); + Ok(()) + } + + fn end(self) -> Result { Ok(()) } + + fn serialize_key(&mut self, _key: &K) -> Result<(), Self::Error> { + Err(SerError::custom("serialize_key not supported; use serialize_entry")) + } + fn serialize_value(&mut self, _value: &V) -> Result<(), Self::Error> { + Err(SerError::custom("serialize_value not supported; use serialize_entry")) + } +} + +// -------------------- seq of pairs support -------------------- + +struct UrlSeqSerializer<'a> { + ser: &'a mut UrlSerializer, +} + +impl<'a> SerializeSeq for UrlSeqSerializer<'a> { + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, element: &T) -> Result<(), Self::Error> { + // Each element must be a (K, V) tuple. + let mut pair = PairSerializer::default(); + element.serialize(&mut pair)?; + let (k, v) = pair.finish()?; + self.ser.push_pair(&k, &v); + Ok(()) + } + + fn end(self) -> Result { Ok(()) } +} + +/// Collects exactly one (key,value) from a serialized 2-tuple. +#[derive(Default)] +struct PairSerializer { + key: Option, + value: Option, + expecting_tuple_len_2: bool, +} + +impl PairSerializer { + fn finish(self) -> Result<(String, String), Error> { + if !self.expecting_tuple_len_2 { + return Err(SerError::custom("expected each element to be a 2-tuple (key, value)")); + } + match (self.key, self.value) { + (Some(k), Some(v)) => Ok((k, v)), + _ => Err(SerError::custom("missing key or value in (key, value) tuple")), + } + } +} + +impl<'a> Serializer for &'a mut PairSerializer { + type Ok = (); + type Error = Error; + + type SerializeTuple = PairTupleSerializer<'a>; + + type SerializeStruct = Impossible<(), Self::Error>; + type SerializeSeq = Impossible<(), Self::Error>; + type SerializeMap = Impossible<(), Self::Error>; + type SerializeTupleStruct = Impossible<(), Self::Error>; + type SerializeTupleVariant = Impossible<(), Self::Error>; + type SerializeStructVariant = Impossible<(), Self::Error>; + + fn serialize_tuple(self, len: usize) -> Result { + if len != 2 { + return Err(SerError::custom("expected tuple length 2 for (key, value)")); + } + self.expecting_tuple_len_2 = true; + Ok(PairTupleSerializer { pair: self, idx: 0 }) + } + + // Anything else is not acceptable for an element. + fn serialize_str(self, _v: &str) -> Result { + Err(SerError::custom("expected (key, value) tuple, got scalar")) + } + fn serialize_bool(self, _v: bool) -> Result { + Err(SerError::custom("expected (key, value) tuple, got scalar")) + } + fn serialize_i32(self, _v: i32) -> Result { + Err(SerError::custom("expected (key, value) tuple, got scalar")) + } + fn serialize_u32(self, _v: u32) -> Result { + Err(SerError::custom("expected (key, value) tuple, got scalar")) + } + + // Keep minimal: + fn serialize_map(self, _len: Option) -> Result { + Err(SerError::custom("expected (key, value) tuple, got map")) + } + fn serialize_seq(self, _len: Option) -> Result { + Err(SerError::custom("expected (key, value) tuple, got seq")) + } + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(SerError::custom("expected (key, value) tuple, got struct")) + } + + // Boilerplate for other methods: + fn serialize_none(self) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_some(self, _value: &T) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_unit(self) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_unit_struct(self, _name: &'static str) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_newtype_struct( + self, + _name: &'static str, + _value: &T, + ) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_char(self, _v: char) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_bytes(self, _v: &[u8]) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(SerError::custom("unsupported")) + } + + fn serialize_i128(self, _v: i128) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_u128(self, _v: u128) -> Result { + Err(SerError::custom("unsupported")) + } + + fn serialize_i8(self, _v: i8) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_i16(self, _v: i16) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_i64(self, _v: i64) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_u8(self, _v: u8) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_u16(self, _v: u16) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_u64(self, _v: u64) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_f32(self, _v: f32) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_f64(self, _v: f64) -> Result { + Err(SerError::custom("unsupported")) + } + + fn collect_str(self, _value: &T) -> Result { + Err(SerError::custom("expected (key, value) tuple, got string")) + } +} + +struct PairTupleSerializer<'a> { + pair: &'a mut PairSerializer, + idx: usize, +} + +impl<'a> SerializeTuple for PairTupleSerializer<'a> { + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> { + match self.idx { + 0 => { + let mut ks = KeySerializer::default(); + value.serialize(&mut ks)?; + self.pair.key = Some(ks.key); + } + 1 => { + let mut vs = ValueSerializer::default(); + value.serialize(&mut vs)?; + self.pair.value = Some(vs.value); + } + _ => return Err(SerError::custom("too many elements in tuple")), + } + self.idx += 1; + Ok(()) + } + + fn end(self) -> Result { Ok(()) } +} + +// -------------------- key/value serializers -------------------- + +#[derive(Default)] +struct KeySerializer { + key: String, +} + +impl<'a> Serializer for &'a mut KeySerializer { + type Ok = (); + type Error = Error; + + type SerializeStruct = Impossible<(), Self::Error>; + type SerializeSeq = Impossible<(), Self::Error>; + type SerializeMap = Impossible<(), Self::Error>; + type SerializeTuple = Impossible<(), Self::Error>; + type SerializeTupleStruct = Impossible<(), Self::Error>; + type SerializeTupleVariant = Impossible<(), Self::Error>; + type SerializeStructVariant = Impossible<(), Self::Error>; + + fn serialize_str(self, v: &str) -> Result<(), Self::Error> { + self.key = percent_encode(v); + Ok(()) + } + + fn serialize_u32(self, v: u32) -> Result<(), Self::Error> { + self.key = v.to_string(); + Ok(()) + } + + fn serialize_i32(self, v: i32) -> Result<(), Self::Error> { + self.key = v.to_string(); + Ok(()) + } + + fn serialize_bool(self, v: bool) -> Result<(), Self::Error> { + self.key = v.to_string(); + Ok(()) + } + + // Everything else: reject to keep keys predictable. + fn serialize_map(self, _len: Option) -> Result { + Err(SerError::custom("map keys must be scalar (string/number/bool)")) + } + fn serialize_seq(self, _len: Option) -> Result { + Err(SerError::custom("map keys must be scalar (string/number/bool)")) + } + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(SerError::custom("map keys must be scalar (string/number/bool)")) + } + + // Boilerplate rejections: + fn serialize_none(self) -> Result<(), Self::Error> { + Err(SerError::custom("key cannot be none")) + } + fn serialize_some(self, _value: &T) -> Result<(), Self::Error> { + Err(SerError::custom("key cannot be option")) + } + fn serialize_unit(self) -> Result<(), Self::Error> { + Err(SerError::custom("key cannot be unit")) + } + fn serialize_char(self, _v: char) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported key type")) + } + fn serialize_bytes(self, _v: &[u8]) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported key type")) + } + fn serialize_unit_struct(self, _name: &'static str) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported key type")) + } + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported key type")) + } + fn serialize_newtype_struct( + self, + _name: &'static str, + _value: &T, + ) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported key type")) + } + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported key type")) + } + fn serialize_tuple(self, _len: usize) -> Result { + Err(SerError::custom("unsupported key type")) + } + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(SerError::custom("unsupported key type")) + } + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(SerError::custom("unsupported key type")) + } + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(SerError::custom("unsupported key type")) + } + + fn serialize_i128(self, _v: i128) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported key type")) + } + fn serialize_u128(self, _v: u128) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported key type")) + } + fn serialize_i8(self, _v: i8) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported key type")) + } + fn serialize_i16(self, _v: i16) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported key type")) + } + fn serialize_i64(self, _v: i64) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported key type")) + } + fn serialize_u8(self, _v: u8) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported key type")) + } + fn serialize_u16(self, _v: u16) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported key type")) + } + fn serialize_u64(self, _v: u64) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported key type")) + } + fn serialize_f32(self, _v: f32) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported key type")) + } + fn serialize_f64(self, _v: f64) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported key type")) + } + + fn collect_str(self, value: &T) -> Result { + self.key = percent_encode(&value.to_string()); + Ok(()) + } +} + +#[derive(Default)] +struct ValueSerializer { + value: String, +} + +impl<'a> Serializer for &'a mut ValueSerializer { + type Ok = (); + type Error = Error; + + type SerializeStruct = Impossible<(), Self::Error>; + type SerializeSeq = Impossible<(), Self::Error>; + type SerializeMap = Impossible<(), Self::Error>; + type SerializeTuple = Impossible<(), Self::Error>; + type SerializeTupleStruct = Impossible<(), Self::Error>; + type SerializeTupleVariant = Impossible<(), Self::Error>; + type SerializeStructVariant = Impossible<(), Self::Error>; + + fn serialize_str(self, v: &str) -> Result<(), Self::Error> { + self.value = percent_encode(v); + Ok(()) + } + + fn serialize_u32(self, v: u32) -> Result<(), Self::Error> { + self.value = v.to_string(); + Ok(()) + } + + fn serialize_i32(self, v: i32) -> Result<(), Self::Error> { + self.value = v.to_string(); + Ok(()) + } + + fn serialize_bool(self, v: bool) -> Result<(), Self::Error> { + self.value = v.to_string(); + Ok(()) + } + + fn serialize_none(self) -> Result<(), Self::Error> { + self.value.clear(); + Ok(()) + } + + // Keep minimal: + fn serialize_map(self, _len: Option) -> Result { + Err(SerError::custom("nested maps not supported")) + } + fn serialize_seq(self, _len: Option) -> Result { + Err(SerError::custom("nested sequences not supported")) + } + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(SerError::custom("nested structs not supported")) + } + + // Boilerplate rejections: + fn serialize_some(self, _value: &T) -> Result<(), Self::Error> { + Err(SerError::custom("option values not supported (except None)")) + } + fn serialize_unit(self) -> Result<(), Self::Error> { Err(SerError::custom("unsupported")) } + fn serialize_char(self, _v: char) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported")) + } + fn serialize_bytes(self, _v: &[u8]) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported")) + } + fn serialize_unit_struct(self, _name: &'static str) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported")) + } + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + ) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported")) + } + fn serialize_newtype_struct( + self, + _name: &'static str, + _value: &T, + ) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported")) + } + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported")) + } + fn serialize_tuple(self, _len: usize) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(SerError::custom("unsupported")) + } + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(SerError::custom("unsupported")) + } + + fn serialize_i128(self, _v: i128) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported")) + } + fn serialize_u128(self, _v: u128) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported")) + } + fn serialize_i8(self, _v: i8) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported")) + } + fn serialize_i16(self, _v: i16) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported")) + } + fn serialize_i64(self, _v: i64) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported")) + } + fn serialize_u8(self, _v: u8) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported")) + } + fn serialize_u16(self, _v: u16) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported")) + } + fn serialize_u64(self, _v: u64) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported")) + } + fn serialize_f32(self, _v: f32) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported")) + } + fn serialize_f64(self, _v: f64) -> Result<(), Self::Error> { + Err(SerError::custom("unsupported")) + } + + fn collect_str(self, value: &T) -> Result { + self.value = percent_encode(&value.to_string()); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use super::*; + + #[test] + fn test_percent_encode_unreserved() { + assert_eq!(percent_encode("abc"), "abc"); + assert_eq!(percent_encode("ABC"), "ABC"); + assert_eq!(percent_encode("123"), "123"); + assert_eq!(percent_encode("a-b_c.d~e"), "a-b_c.d~e"); + } + + #[test] + fn test_percent_encode_space() { + assert_eq!(percent_encode("hello world"), "hello+world"); + assert_eq!(percent_encode("a b c"), "a+b+c"); + } + + #[test] + fn test_percent_encode_special_chars() { + assert_eq!(percent_encode("a&b"), "a%26b"); + assert_eq!(percent_encode("a=b"), "a%3Db"); + assert_eq!(percent_encode("a+b"), "a%2Bb"); + assert_eq!(percent_encode("a?b"), "a%3Fb"); + assert_eq!(percent_encode("a/b"), "a%2Fb"); + assert_eq!(percent_encode("a#b"), "a%23b"); + } + + #[test] + fn test_percent_encode_unicode() { + assert_eq!(percent_encode("café"), "caf%C3%A9"); + assert_eq!(percent_encode("日本"), "%E6%97%A5%E6%9C%AC"); + } + + #[test] + fn test_to_string_btreemap() { + let mut map = BTreeMap::new(); + map.insert("name", "alice"); + map.insert("age", "30"); + + let result = to_string(&map).unwrap(); + // BTreeMap is sorted, so order is deterministic + assert_eq!(result, "age=30&name=alice"); + } + + #[test] + fn test_to_string_btreemap_with_spaces() { + let mut map = BTreeMap::new(); + map.insert("greeting", "hello world"); + + let result = to_string(&map).unwrap(); + assert_eq!(result, "greeting=hello+world"); + } + + #[test] + fn test_to_string_btreemap_with_special_chars() { + let mut map = BTreeMap::new(); + map.insert("query", "a&b=c"); + + let result = to_string(&map).unwrap(); + assert_eq!(result, "query=a%26b%3Dc"); + } + + #[test] + fn test_to_string_vec_of_tuples() { + let pairs = vec![("foo", "bar"), ("baz", "qux")]; + + let result = to_string(&pairs).unwrap(); + assert_eq!(result, "foo=bar&baz=qux"); + } + + #[test] + fn test_to_string_vec_of_tuples_with_encoding() { + let pairs = vec![("key", "value with spaces"), ("special", "a&b")]; + + let result = to_string(&pairs).unwrap(); + assert_eq!(result, "key=value+with+spaces&special=a%26b"); + } + + #[test] + fn test_to_string_empty_map() { + let map: BTreeMap<&str, &str> = BTreeMap::new(); + + let result = to_string(&map).unwrap(); + assert_eq!(result, ""); + } + + #[test] + fn test_to_string_empty_vec() { + let pairs: Vec<(&str, &str)> = vec![]; + + let result = to_string(&pairs).unwrap(); + assert_eq!(result, ""); + } + + #[test] + fn test_to_string_numeric_values() { + let mut map = BTreeMap::new(); + map.insert("count", 42u32); + map.insert("negative", 0u32); // Can't use negative with u32 + + let result = to_string(&map).unwrap(); + assert_eq!(result, "count=42&negative=0"); + } + + #[test] + fn test_to_string_bool_values() { + let mut map = BTreeMap::new(); + map.insert("enabled", true); + map.insert("disabled", false); + + let result = to_string(&map).unwrap(); + assert_eq!(result, "disabled=false&enabled=true"); + } + + #[test] + fn test_error_top_level_string() { + let result = to_string(&"just a string"); + assert!(result.is_err()); + } + + #[test] + fn test_error_top_level_number() { + let result = to_string(&42i32); + assert!(result.is_err()); + } +} diff --git a/bitreq/tests/main.rs b/bitreq/tests/main.rs index d7b55af28..b01bf0e86 100644 --- a/bitreq/tests/main.rs +++ b/bitreq/tests/main.rs @@ -32,6 +32,46 @@ async fn test_json_using_serde() { assert_eq!(actual_json, original_json); } +#[tokio::test] +#[cfg(feature = "forms")] +async fn test_with_form() { + use std::collections::HashMap; + + setup(); + let mut form_data = HashMap::new(); + form_data.insert("name", "test"); + form_data.insert("value", "42"); + + let response = make_request( + bitreq::post(url("/echo")).with_form(&form_data).expect("form encoding failed"), + ) + .await; + let body = response.as_str().expect("response body should be valid UTF-8"); + // Form data is URL encoded, order may vary due to HashMap + assert!(body.contains("name=test")); + assert!(body.contains("value=42")); +} + +#[tokio::test] +#[cfg(feature = "forms")] +async fn test_with_form_special_chars() { + use std::collections::HashMap; + + setup(); + let mut form_data = HashMap::new(); + form_data.insert("message", "hello world"); + form_data.insert("special", "a&b=c"); + + let response = make_request( + bitreq::post(url("/echo")).with_form(&form_data).expect("form encoding failed"), + ) + .await; + let body = response.as_str().expect("response body should be valid UTF-8"); + // Special characters should be URL encoded + assert!(body.contains("message=hello+world") || body.contains("message=hello%20world")); + assert!(body.contains("special=a%26b%3Dc")); +} + #[tokio::test] async fn test_timeout_too_low() { setup();