use crate::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum Scheme {
#[default]
None,
Http,
Https,
Cli,
Repl,
Other(String),
}
impl Scheme {
pub fn from_str(scheme: &str) -> Self {
match scheme.to_ascii_lowercase().as_str() {
"http" => Self::Http,
"https" => Self::Https,
"cli" => Self::Cli,
"repl" => Self::Repl,
"" => Self::None,
other => Self::Other(other.to_string()),
}
}
pub fn as_str(&self) -> &str {
match self {
Self::None => "",
Self::Http => "http",
Self::Https => "https",
Self::Cli => "cli",
Self::Repl => "repl",
Self::Other(scheme) => scheme.as_str(),
}
}
pub fn is_http(&self) -> bool { matches!(self, Self::Http | Self::Https) }
pub fn is_cli(&self) -> bool { matches!(self, Self::Cli | Self::Repl) }
}
impl std::fmt::Display for Scheme {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}", self.as_str())
}
}
#[cfg(feature = "http")]
impl From<&http::uri::Scheme> for Scheme {
fn from(scheme: &http::uri::Scheme) -> Self {
Self::from_str(scheme.as_str())
}
}
#[cfg(feature = "http")]
impl From<Option<&http::uri::Scheme>> for Scheme {
fn from(scheme: Option<&http::uri::Scheme>) -> Self {
scheme.map(Self::from).unwrap_or(Self::None)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Parts {
scheme: Scheme,
authority: String,
path: Vec<String>,
params: MultiMap<String, String>,
headers: MultiMap<String, String>,
version: String,
}
const DEFAULT_HTTP_VERSION: &str = "1.1";
const DEFAULT_CLI_VERSION: &str = "0.1.0";
impl Default for Parts {
fn default() -> Self {
Self {
scheme: Scheme::None,
authority: String::new(),
path: Vec::new(),
params: default(),
headers: default(),
version: DEFAULT_HTTP_VERSION.to_string(),
}
}
}
impl Parts {
pub fn new() -> Self { Self::default() }
pub fn scheme(&self) -> &Scheme { &self.scheme }
pub fn authority(&self) -> &str { &self.authority }
pub fn path(&self) -> &Vec<String> { &self.path }
pub fn version(&self) -> &str { &self.version }
pub fn params(&self) -> &MultiMap<String, String> { &self.params }
pub fn params_mut(&mut self) -> &mut MultiMap<String, String> {
&mut self.params
}
pub fn headers_mut(&mut self) -> &mut MultiMap<String, String> {
&mut self.headers
}
pub fn insert_param(
&mut self,
key: impl Into<String>,
value: impl Into<String>,
) {
self.params.insert(key.into(), value.into());
}
pub fn insert_header(
&mut self,
key: impl Into<String>,
value: impl Into<String>,
) {
self.headers.insert(key.into(), value.into());
}
pub fn headers(&self) -> &MultiMap<String, String> { &self.headers }
pub fn get_param(&self, key: &str) -> Option<&String> {
self.params.get_vec(key).and_then(|vals| vals.first())
}
pub fn get_params(&self, key: &str) -> Option<&Vec<String>> {
self.params.get_vec(key)
}
pub fn get_header(&self, key: &str) -> Option<&String> {
self.headers.get_vec(key).and_then(|vals| vals.first())
}
pub fn get_headers(&self, key: &str) -> Option<&Vec<String>> {
self.headers.get_vec(key)
}
pub fn has_param(&self, key: &str) -> bool { self.params.contains_key(key) }
pub fn has_header(&self, key: &str) -> bool {
self.headers.contains_key(key)
}
pub fn has_body(&self) -> bool {
self.get_header("content-length")
.and_then(|val| val.parse::<usize>().ok())
.map(|len| len > 0)
.unwrap_or(false)
|| self
.get_header("transfer-encoding")
.map(|val| val.contains("chunked"))
.unwrap_or(false)
}
pub fn path_string(&self) -> String {
if self.path.is_empty() {
"/".to_string()
} else {
format!("/{}", self.path.join("/"))
}
}
pub fn query_string(&self) -> String { build_query_string(&self.params) }
pub fn uri(&self) -> String {
let path = self.path_string();
let query = self.query_string();
let base = match (&self.scheme, self.authority.is_empty()) {
(Scheme::None, _) | (_, true) => path,
(scheme, false) => {
format!("{}://{}{}", scheme.as_str(), self.authority, path)
}
};
if query.is_empty() {
base
} else {
format!("{}?{}", base, query)
}
}
pub fn first_segment(&self) -> Option<&str> {
self.path.first().map(|segment| segment.as_str())
}
pub fn last_segment(&self) -> Option<&str> {
self.path.last().map(|segment| segment.as_str())
}
pub fn path_from(&self, index: usize) -> &[String] {
if index >= self.path.len() {
&[]
} else {
&self.path[index..]
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PartsBuilder {
scheme: Scheme,
authority: String,
path: Vec<String>,
params: MultiMap<String, String>,
headers: MultiMap<String, String>,
version: Option<String>,
}
impl PartsBuilder {
pub fn new() -> Self { Self::default() }
pub fn scheme(mut self, scheme: Scheme) -> Self {
self.scheme = scheme;
self
}
pub fn authority(mut self, authority: impl Into<String>) -> Self {
self.authority = authority.into();
self
}
pub fn path(mut self, path: Vec<String>) -> Self {
self.path = path;
self
}
pub fn path_str(mut self, path: &str) -> Self {
self.path = split_path(path);
self
}
pub fn param(
mut self,
key: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.params.insert(key.into(), value.into());
self
}
pub fn flag(mut self, key: impl Into<String>) -> Self {
let key = key.into();
if !self.params.contains_key(&key) {
self.params.insert(key, String::new());
}
self
}
pub fn header(
mut self,
key: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.headers.insert(key.into(), value.into());
self
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.version = Some(version.into());
self
}
pub fn build(self) -> Parts {
Parts {
scheme: self.scheme,
authority: self.authority,
path: self.path,
params: self.params,
headers: self.headers,
version: self
.version
.unwrap_or_else(|| DEFAULT_HTTP_VERSION.to_string()),
}
}
pub fn build_request_parts(self, method: HttpMethod) -> RequestParts {
RequestParts {
method,
parts: self.build(),
}
}
pub fn build_response_parts(self, status: StatusCode) -> ResponseParts {
ResponseParts {
status,
parts: self.build(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RequestParts {
method: HttpMethod,
parts: Parts,
}
impl Default for RequestParts {
fn default() -> Self {
Self {
method: HttpMethod::Get,
parts: Parts::default(),
}
}
}
impl RequestParts {
pub fn new(method: HttpMethod, path: impl AsRef<str>) -> Self {
let path_str = path.as_ref();
if path_str.contains("://") {
#[cfg(feature = "http")]
if let Ok(uri) = path_str.parse::<http::Uri>() {
let scheme = Scheme::from(uri.scheme());
let authority = uri
.authority()
.map(|auth| auth.to_string())
.unwrap_or_default();
let path_segments = split_path(uri.path());
let params =
uri.query().map(parse_query_string).unwrap_or_default();
return Self {
method,
parts: Parts {
scheme,
authority,
path: path_segments,
params,
headers: MultiMap::<String, String>::default(),
version: DEFAULT_HTTP_VERSION.to_string(),
},
};
}
}
let (path_only, query_str) = split_path_and_query(path_str);
let path_segments = split_path(path_only);
let params = query_str.map(parse_query_string).unwrap_or_default();
Self {
method,
parts: Parts {
scheme: Scheme::None,
authority: String::new(),
path: path_segments,
params,
headers: MultiMap::<String, String>::default(),
version: DEFAULT_HTTP_VERSION.to_string(),
},
}
}
pub fn get(path: impl AsRef<str>) -> Self {
Self::new(HttpMethod::Get, path)
}
pub fn post(path: impl AsRef<str>) -> Self {
Self::new(HttpMethod::Post, path)
}
pub fn put(path: impl AsRef<str>) -> Self {
Self::new(HttpMethod::Put, path)
}
pub fn delete(path: impl AsRef<str>) -> Self {
Self::new(HttpMethod::Delete, path)
}
pub fn patch(path: impl AsRef<str>) -> Self {
Self::new(HttpMethod::Patch, path)
}
pub fn method(&self) -> &HttpMethod { &self.method }
pub fn parts(&self) -> &Parts { &self.parts }
pub fn parts_mut(&mut self) -> &mut Parts { &mut self.parts }
pub fn into_parts(self) -> Parts { self.parts }
pub fn with_method(mut self, method: HttpMethod) -> Self {
self.method = method;
self
}
}
impl std::ops::Deref for RequestParts {
type Target = Parts;
fn deref(&self) -> &Self::Target { &self.parts }
}
impl std::ops::DerefMut for RequestParts {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.parts }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResponseParts {
pub status: StatusCode,
pub parts: Parts,
}
impl Default for ResponseParts {
fn default() -> Self {
Self {
status: StatusCode::Ok,
parts: Parts::default(),
}
}
}
impl ResponseParts {
pub fn new(status: StatusCode) -> Self {
Self {
status,
parts: Parts::default(),
}
}
pub fn ok() -> Self { Self::new(StatusCode::Ok) }
pub fn not_found() -> Self { Self::new(StatusCode::NotFound) }
pub fn internal_error() -> Self { Self::new(StatusCode::InternalError) }
pub fn bad_request() -> Self { Self::new(StatusCode::MalformedRequest) }
pub fn status(&self) -> StatusCode { self.status }
pub fn status_to_exit_code(&self) -> Result<(), std::num::NonZeroU8> {
self.status().to_exit_code()
}
pub fn parts_mut(&mut self) -> &mut Parts { &mut self.parts }
pub fn into_parts(self) -> Parts { self.parts }
pub fn with_status(mut self, status: StatusCode) -> Self {
self.status = status;
self
}
}
impl std::ops::Deref for ResponseParts {
type Target = Parts;
fn deref(&self) -> &Self::Target { &self.parts }
}
impl std::ops::DerefMut for ResponseParts {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.parts }
}
fn split_path_and_query(uri: &str) -> (&str, Option<&str>) {
uri.split_once('?')
.map(|(path, query)| (path, Some(query)))
.unwrap_or((uri, None))
}
#[cfg(feature = "http")]
fn header_map_to_multimap(map: &http::HeaderMap) -> MultiMap<String, String> {
use heck::ToKebabCase;
let mut multi_map = MultiMap::default();
for (key, value) in map.iter() {
let key = key.to_string().to_kebab_case();
let value = value.to_str().unwrap_or("<opaque-bytes>").to_string();
multi_map.insert(key, value);
}
multi_map
}
fn parse_query_string(query: &str) -> MultiMap<String, String> {
let mut params = MultiMap::default();
for pair in query.split('&') {
if pair.is_empty() {
continue;
}
let (key, value) = match pair.split_once('=') {
Some((key, value)) => (key.to_string(), value.to_string()),
None => (pair.to_string(), String::new()),
};
params.insert(key, value);
}
params
}
fn split_path(path: &str) -> Vec<String> {
path.split('/')
.filter(|segment| !segment.is_empty())
.map(|segment| segment.to_string())
.collect()
}
#[cfg(feature = "http")]
fn multimap_to_header_map(
multimap: &MultiMap<String, String>,
) -> Result<http::HeaderMap, http::header::InvalidHeaderValue> {
use std::str::FromStr;
let mut headers = http::HeaderMap::new();
for (key, values) in multimap.iter_all() {
let header_name = http::header::HeaderName::from_str(key)
.unwrap_or_else(|_| {
http::header::HeaderName::from_static("x-invalid")
});
for value in values {
headers.append(
header_name.clone(),
http::header::HeaderValue::from_str(value)?,
);
}
}
Ok(headers)
}
fn build_query_string(params: &MultiMap<String, String>) -> String {
let mut parts = Vec::new();
for (key, values) in params.iter_all() {
for value in values {
if value.is_empty() {
parts.push(key.clone());
} else {
parts.push(format!("{}={}", key, value));
}
}
}
parts.join("&")
}
#[cfg(feature = "http")]
impl From<http::request::Parts> for RequestParts {
fn from(http_parts: http::request::Parts) -> Self {
let uri = &http_parts.uri;
let scheme = Scheme::from(uri.scheme());
let authority = uri
.authority()
.map(|auth| auth.to_string())
.unwrap_or_default();
let path = split_path(uri.path());
let params = uri.query().map(parse_query_string).unwrap_or_default();
let headers = header_map_to_multimap(&http_parts.headers);
let version = http_ext::version_to_string(http_parts.version);
let method = HttpMethod::from(http_parts.method);
RequestParts {
method,
parts: Parts {
scheme,
authority,
path,
params,
headers,
version,
},
}
}
}
#[cfg(feature = "http")]
impl From<&http::request::Parts> for RequestParts {
fn from(http_parts: &http::request::Parts) -> Self {
let uri = &http_parts.uri;
let scheme = Scheme::from(uri.scheme());
let authority = uri
.authority()
.map(|auth| auth.to_string())
.unwrap_or_default();
let path = split_path(uri.path());
let params = uri.query().map(parse_query_string).unwrap_or_default();
let headers = header_map_to_multimap(&http_parts.headers);
let version = http_ext::version_to_string(http_parts.version);
let method = HttpMethod::from(&http_parts.method);
RequestParts {
method,
parts: Parts {
scheme,
authority,
path,
params,
headers,
version,
},
}
}
}
#[cfg(feature = "http")]
impl From<http::response::Parts> for ResponseParts {
fn from(http_parts: http::response::Parts) -> Self {
let headers = header_map_to_multimap(&http_parts.headers);
let version = http_ext::version_to_string(http_parts.version);
ResponseParts {
status: StatusCode::from(http_parts.status),
parts: Parts {
scheme: Scheme::None,
authority: String::new(),
path: Vec::new(),
params: MultiMap::default(),
headers,
version,
},
}
}
}
#[cfg(feature = "http")]
impl From<&http::response::Parts> for ResponseParts {
fn from(http_parts: &http::response::Parts) -> Self {
let headers = header_map_to_multimap(&http_parts.headers);
let version = http_ext::version_to_string(http_parts.version);
ResponseParts {
status: StatusCode::from(http_parts.status),
parts: Parts {
scheme: Scheme::None,
authority: String::new(),
path: Vec::new(),
params: MultiMap::default(),
headers,
version,
},
}
}
}
impl From<CliArgs> for RequestParts {
fn from(cli: CliArgs) -> Self {
let path = cli.path.clone();
let mut params = MultiMap::default();
for (key, values) in &cli.query {
if values.is_empty() {
params.insert(key.clone(), String::new());
} else {
for value in values {
params.insert(key.clone(), value.clone());
}
}
}
RequestParts {
method: HttpMethod::Get, parts: Parts {
scheme: Scheme::Cli,
authority: env_ext::var("CARGO_PKG_NAME").unwrap_or_default(),
path,
params,
headers: MultiMap::default(),
version: env_ext::var("CARGO_PKG_VERSION")
.unwrap_or_else(|_| DEFAULT_CLI_VERSION.to_string()),
},
}
}
}
impl From<&CliArgs> for RequestParts {
fn from(cli: &CliArgs) -> Self {
let path = cli.path.clone();
let mut params = MultiMap::default();
for (key, values) in &cli.query {
if values.is_empty() {
params.insert(key.clone(), String::new());
} else {
for value in values {
params.insert(key.clone(), value.clone());
}
}
}
RequestParts {
method: HttpMethod::Get,
parts: Parts {
scheme: Scheme::Cli,
authority: env_ext::var("CARGO_PKG_NAME").unwrap_or_default(),
path,
params,
headers: MultiMap::default(),
version: DEFAULT_CLI_VERSION.to_string(),
},
}
}
}
#[cfg(feature = "http")]
impl TryFrom<RequestParts> for http::request::Parts {
type Error = http::Error;
fn try_from(parts: RequestParts) -> Result<Self, Self::Error> {
let method: http::Method = parts.method.into();
let uri_str = parts.parts.uri();
let mut builder = http::Request::builder()
.method(method)
.uri(&uri_str)
.version(http_ext::parse_version(&parts.parts.version));
if let Ok(header_map) = multimap_to_header_map(&parts.parts.headers) {
for (key, value) in header_map.iter() {
builder = builder.header(key, value);
}
}
let (http_parts, _) = builder.body(())?.into_parts();
Ok(http_parts)
}
}
#[cfg(feature = "http")]
impl TryFrom<ResponseParts> for http::response::Parts {
type Error = http::Error;
fn try_from(parts: ResponseParts) -> Result<Self, Self::Error> {
let mut builder = http::Response::builder()
.status(parts.status)
.version(http_ext::parse_version(&parts.parts.version));
if let Ok(header_map) = multimap_to_header_map(&parts.parts.headers) {
for (key, value) in header_map.iter() {
builder = builder.header(key, value);
}
}
let (http_parts, _) = builder.body(())?.into_parts();
Ok(http_parts)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parts_default() {
let parts = Parts::default();
parts.path().xpect_empty();
parts.uri().xpect_eq("/");
parts.scheme().clone().xpect_eq(Scheme::None);
parts.version().xpect_eq("1.1");
}
#[test]
fn parts_builder() {
let parts = PartsBuilder::new()
.scheme(Scheme::Http)
.authority("example.com")
.path_str("/api/users/123")
.param("limit", "10")
.header("content-type", "application/json")
.build();
parts.scheme().clone().xpect_eq(Scheme::Http);
parts.authority().xpect_eq("example.com");
parts.path().xpect_eq(vec![
"api".to_string(),
"users".to_string(),
"123".to_string(),
]);
parts.get_param("limit").unwrap().xpect_eq("10");
parts
.get_header("content-type")
.unwrap()
.xpect_eq("application/json");
}
#[test]
fn parts_builder_request_parts() {
let parts = PartsBuilder::new()
.path_str("/api/users")
.param("page", "1")
.build_request_parts(HttpMethod::Post);
(*parts.method()).xpect_eq(HttpMethod::Post);
parts
.path()
.xpect_eq(vec!["api".to_string(), "users".to_string()]);
parts.get_param("page").unwrap().xpect_eq("1");
}
#[test]
#[cfg(feature = "http")]
fn parts_builder_response_parts() {
let parts = PartsBuilder::new()
.header("content-type", "text/html")
.build_response_parts(StatusCode::Ok);
parts.status().xpect_eq(StatusCode::Ok);
parts
.get_header("content-type")
.unwrap()
.xpect_eq("text/html");
}
#[test]
fn request_parts_new() {
let parts = RequestParts::get("/api/users");
(*parts.method()).xpect_eq(HttpMethod::Get);
parts
.path()
.xpect_eq(vec!["api".to_string(), "users".to_string()]);
}
#[test]
fn request_parts_post() {
let parts = RequestParts::post("/api/users");
(*parts.method()).xpect_eq(HttpMethod::Post);
}
#[test]
fn response_parts_default() {
let parts = ResponseParts::default();
parts.status().xpect_eq(StatusCode::Ok);
}
#[test]
fn response_parts_not_found() {
let parts = ResponseParts::not_found();
parts.status().xpect_eq(StatusCode::NotFound);
}
#[test]
#[cfg(feature = "http")]
fn from_http_request_parts() {
let http_parts = http::Request::builder()
.method(http::Method::POST)
.uri("https://example.com/api/users?limit=10&offset=20")
.header("content-type", "application/json")
.body(())
.unwrap()
.into_parts()
.0;
let parts = RequestParts::from(http_parts);
(*parts.method()).xpect_eq(HttpMethod::Post);
parts.scheme().clone().xpect_eq(Scheme::Https);
parts.authority().xpect_eq("example.com");
parts
.path()
.xpect_eq(vec!["api".to_string(), "users".to_string()]);
parts.get_param("limit").unwrap().xpect_eq("10");
parts.get_param("offset").unwrap().xpect_eq("20");
parts
.get_header("content-type")
.unwrap()
.xpect_eq("application/json");
}
#[test]
#[cfg(feature = "http")]
fn from_http_response_parts() {
let http_parts = http::Response::builder()
.status(http::StatusCode::CREATED)
.header("content-type", "application/json")
.body(())
.unwrap()
.into_parts()
.0;
let parts = ResponseParts::from(http_parts);
parts.status().xpect_eq(StatusCode::Created);
parts
.get_header("content-type")
.unwrap()
.xpect_eq("application/json");
}
#[test]
fn from_cli_args() {
let cli = CliArgs::parse("users list --limit 10 --verbose");
let parts = RequestParts::from(cli);
parts.scheme().clone().xpect_eq(Scheme::Cli);
(*parts.method()).xpect_eq(HttpMethod::Get);
parts
.path()
.xpect_eq(vec!["users".to_string(), "list".to_string()]);
parts.get_param("limit").unwrap().xpect_eq("10");
parts.has_param("verbose").xpect_true();
}
#[test]
fn from_cli_args_flags_only() {
let cli = CliArgs::parse("--verbose --debug");
let parts = RequestParts::from(cli);
parts.path().xpect_empty();
parts.has_param("verbose").xpect_true();
parts.has_param("debug").xpect_true();
}
#[test]
fn path_string() {
let parts = PartsBuilder::new().path_str("/api/users/123").build();
parts.path_string().xpect_eq("/api/users/123");
let empty_parts = Parts::default();
empty_parts.path_string().xpect_eq("/");
}
#[test]
fn query_string() {
let parts = PartsBuilder::new()
.param("limit", "10")
.param("offset", "20")
.build();
let query = parts.query_string();
(&query).xpect_contains("limit=10");
(&query).xpect_contains("offset=20");
}
#[test]
fn uri_construction() {
let parts = PartsBuilder::new()
.path_str("/api/users")
.param("page", "1")
.build();
let uri = parts.uri();
(&uri).xpect_starts_with("/api/users?");
(&uri).xpect_contains("page=1");
}
#[test]
fn path_segments() {
let parts = PartsBuilder::new().path_str("/api/users/123").build();
parts.first_segment().unwrap().xpect_eq("api");
parts.last_segment().unwrap().xpect_eq("123");
parts.path_from(1).xpect_eq(["users", "123"]);
parts.path_from(10).len().xpect_eq(0);
}
#[test]
fn split_path_handles_edge_cases() {
split_path("").xpect_empty();
split_path("/").xpect_empty();
split_path("//").xpect_empty();
split_path("/a//b/").xpect_eq(vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn scheme_parsing() {
Scheme::from_str("http").xpect_eq(Scheme::Http);
Scheme::from_str("HTTPS").xpect_eq(Scheme::Https);
Scheme::from_str("cli").xpect_eq(Scheme::Cli);
Scheme::from_str("").xpect_eq(Scheme::None);
Scheme::from_str("custom")
.xpect_eq(Scheme::Other("custom".to_string()));
}
#[test]
#[cfg(feature = "http")]
fn request_parts_to_http() {
let parts = PartsBuilder::new()
.path_str("/api/users")
.param("limit", "10")
.header("content-type", "application/json")
.build_request_parts(HttpMethod::Post);
let http_parts: http::request::Parts = parts.try_into().unwrap();
http_parts.method.xpect_eq(http::Method::POST);
http_parts.uri.path().xpect_eq("/api/users");
http_parts.uri.query().unwrap().xpect_eq("limit=10");
}
#[test]
#[cfg(feature = "http")]
fn response_parts_to_http() {
let parts = PartsBuilder::new()
.header("content-type", "application/json")
.build_response_parts(StatusCode::Http(http::StatusCode::CREATED));
let http_parts: http::response::Parts = parts.try_into().unwrap();
http_parts.status.xpect_eq(http::StatusCode::CREATED);
}
#[test]
fn request_parts_deref() {
let parts = RequestParts::get("/api/users");
parts.path().len().xpect_eq(2);
parts.path_string().xpect_eq("/api/users");
}
#[test]
fn response_parts_deref() {
let mut parts = ResponseParts::ok();
parts
.parts_mut()
.headers
.insert("x-custom".to_string(), "value".to_string());
parts.get_header("x-custom").unwrap().xpect_eq("value");
}
#[test]
fn has_body_detection() {
let mut parts = Parts::default();
parts.has_body().xpect_false();
parts.insert_header("content-length", "5");
parts.has_body().xpect_true();
let mut parts2 = Parts::default();
parts2.insert_header("transfer-encoding", "chunked");
parts2.has_body().xpect_true();
}
}