use std::borrow::Cow;
use chrono::{DateTime, NaiveDate, Utc};
use serde_json::Value;
use url::Url;
use crate::api::BodyError;
pub trait ParamValue<'a> {
#[allow(clippy::wrong_self_convention)]
fn as_value(&self) -> Cow<'a, str>;
}
impl ParamValue<'static> for bool {
fn as_value(&self) -> Cow<'static, str> {
if *self {
"true".into()
} else {
"false".into()
}
}
}
impl<'a> ParamValue<'a> for &'a str {
fn as_value(&self) -> Cow<'a, str> {
(*self).into()
}
}
impl ParamValue<'static> for String {
fn as_value(&self) -> Cow<'static, str> {
self.clone().into()
}
}
impl<'a> ParamValue<'a> for &'a String {
fn as_value(&self) -> Cow<'a, str> {
(*self).into()
}
}
impl<'a> ParamValue<'a> for Cow<'a, str> {
fn as_value(&self) -> Cow<'a, str> {
self.clone()
}
}
impl<'a, 'b: 'a> ParamValue<'a> for &'b Cow<'a, str> {
fn as_value(&self) -> Cow<'a, str> {
(*self).clone()
}
}
impl ParamValue<'static> for u64 {
fn as_value(&self) -> Cow<'static, str> {
self.to_string().into()
}
}
impl ParamValue<'static> for f64 {
fn as_value(&self) -> Cow<'static, str> {
self.to_string().into()
}
}
impl ParamValue<'static> for DateTime<Utc> {
fn as_value(&self) -> Cow<'static, str> {
self.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
.into()
}
}
impl ParamValue<'static> for NaiveDate {
fn as_value(&self) -> Cow<'static, str> {
format!("{}", self.format("%Y-%m-%d")).into()
}
}
#[derive(Debug, Default, Clone)]
pub struct FormParams<'a> {
params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
}
impl<'a> FormParams<'a> {
pub fn push<'b, K, V>(&mut self, key: K, value: V) -> &mut Self
where
K: Into<Cow<'a, str>>,
V: ParamValue<'b>,
'b: 'a,
{
self.params.push((key.into(), value.as_value()));
self
}
pub fn push_opt<'b, K, V>(&mut self, key: K, value: Option<V>) -> &mut Self
where
K: Into<Cow<'a, str>>,
V: ParamValue<'b>,
'b: 'a,
{
if let Some(value) = value {
self.params.push((key.into(), value.as_value()));
}
self
}
pub fn extend<'b, I, K, V>(&mut self, iter: I) -> &mut Self
where
I: Iterator<Item = (K, V)>,
K: Into<Cow<'a, str>>,
V: ParamValue<'b>,
'b: 'a,
{
self.params
.extend(iter.map(|(key, value)| (key.into(), value.as_value())));
self
}
pub fn into_body(self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
let body = serde_urlencoded::to_string(self.params)?;
Ok(Some((
"application/x-www-form-urlencoded",
body.into_bytes(),
)))
}
}
#[derive(Debug, Default, Clone)]
#[non_exhaustive]
pub struct JsonParams {}
impl JsonParams {
pub fn clean(mut val: Value) -> Value {
if let Some(obj) = val.as_object_mut() {
obj.retain(|_, v| {
!v.is_null()
&& v.as_array().map(|a| !a.is_empty()).unwrap_or(true)
&& v.as_object().map(|o| !o.is_empty()).unwrap_or(true)
});
}
val
}
pub fn into_body(input: &Value) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
let body = serde_json::to_string(input)?;
Ok(Some(("application/json", body.into_bytes())))
}
}
#[derive(Debug, Default, Clone)]
pub struct QueryParams<'a> {
params: Vec<(Cow<'a, str>, Cow<'a, str>)>,
}
impl<'a> QueryParams<'a> {
pub fn push<'b, K, V>(&mut self, key: K, value: V) -> &mut Self
where
K: Into<Cow<'a, str>>,
V: ParamValue<'b>,
'b: 'a,
{
self.params.push((key.into(), value.as_value()));
self
}
pub fn push_opt<'b, K, V>(&mut self, key: K, value: Option<V>) -> &mut Self
where
K: Into<Cow<'a, str>>,
V: ParamValue<'b>,
'b: 'a,
{
if let Some(value) = value {
self.params.push((key.into(), value.as_value()));
}
self
}
pub fn extend<'b, I, K, V>(&mut self, iter: I) -> &mut Self
where
I: Iterator<Item = (K, V)>,
K: Into<Cow<'a, str>>,
V: ParamValue<'b>,
'b: 'a,
{
self.params
.extend(iter.map(|(key, value)| (key.into(), value.as_value())));
self
}
pub fn add_to_url(&self, url: &mut Url) {
let mut pairs = url.query_pairs_mut();
pairs.extend_pairs(self.params.iter());
}
}
fn quote_header_param(s: &str) -> Vec<u8> {
let mut out = Vec::with_capacity(s.len());
for b in s.bytes() {
if b == b'"' || b == b'\\' {
out.push(b'\\');
}
out.push(b);
}
out
}
pub struct MultipartBuilder {
boundary: String,
body: Vec<u8>,
}
impl MultipartBuilder {
pub fn new(boundary: &str) -> Self {
Self {
boundary: boundary.to_owned(),
body: Vec::new(),
}
}
pub fn content_type(&self) -> String {
format!("multipart/form-data; boundary={}", self.boundary)
}
fn start_part(&mut self, name: &str) {
if self.body.is_empty() {
self.body.extend_from_slice(b"--");
} else {
self.body.extend_from_slice(b"\r\n--");
}
self.body.extend_from_slice(self.boundary.as_bytes());
self.body
.extend_from_slice(b"\r\nContent-Disposition: form-data; name=\"");
self.body.extend_from_slice(name.as_bytes());
self.body.push(b'"');
}
pub fn text_field<'b, V: ParamValue<'b>>(&mut self, name: &str, value: V) {
self.start_part(name);
self.body.extend_from_slice(b"\r\n\r\n");
self.body.extend_from_slice(value.as_value().as_bytes());
}
pub fn text_field_opt<'b, V: ParamValue<'b>>(&mut self, name: &str, value: Option<V>) {
if let Some(v) = value {
self.text_field(name, v);
}
}
pub fn file_field(&mut self, name: &str, filename: &str, data: &[u8]) {
self.start_part(name);
self.body.extend_from_slice(b"; filename=\"");
self.body.extend_from_slice("e_header_param(filename));
self.body
.extend_from_slice(b"\"\r\nContent-Type: application/octet-stream\r\n\r\n");
self.body.extend_from_slice(data);
}
pub fn finish(mut self) -> Vec<u8> {
self.body.extend_from_slice(b"\r\n--");
self.body.extend_from_slice(self.boundary.as_bytes());
self.body.extend_from_slice(b"--\r\n");
self.body
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use crate::api::{JsonParams, MultipartBuilder, ParamValue};
#[test]
fn bool_str() {
let items = &[(true, "true"), (false, "false")];
for (i, s) in items {
assert_eq!((*i).as_value(), *s);
}
}
#[test]
fn test_str_as_value() {
let items = &["foo", "bar"];
for i in items {
assert_eq!(i.as_value(), *i);
}
}
#[test]
fn test_string_as_value() {
let items = &["foo", "bar"];
for i in items {
let s = String::from(*i);
assert_eq!(s.as_value(), s);
}
}
#[test]
fn json_params_clean() {
let dirty = json!({
"null": null,
"int": 1,
"str": "str",
"array": [null],
"empty_array": [],
"object": {
"nested_null": null,
"nested_empty_array": [],
"nested_empty_object": {},
},
"empty_object": {},
});
let clean = json!({
"int": 1,
"str": "str",
"array": [null],
"object": {
"nested_null": null,
"nested_empty_array": [],
"nested_empty_object": {},
},
});
assert_eq!(JsonParams::clean(dirty), clean);
}
#[test]
fn multipart_text_field() {
let mut m = MultipartBuilder::new("boundary");
m.text_field("key", "value");
let body = m.finish();
assert_eq!(
body,
b"--boundary\r\nContent-Disposition: form-data; name=\"key\"\r\n\r\nvalue\r\n--boundary--\r\n",
);
}
#[test]
fn multipart_multiple_text_fields() {
let mut m = MultipartBuilder::new("boundary");
m.text_field("a", "1");
m.text_field("b", "2");
let body = m.finish();
assert_eq!(
body,
b"--boundary\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\n1\
\r\n--boundary\r\nContent-Disposition: form-data; name=\"b\"\r\n\r\n2\
\r\n--boundary--\r\n",
);
}
#[test]
fn multipart_file_field() {
let mut m = MultipartBuilder::new("boundary");
m.file_field("upload", "file.tar.gz", b"\x1f\x8b");
let body = m.finish();
assert_eq!(
body,
b"--boundary\r\nContent-Disposition: form-data; name=\"upload\"; filename=\"file.tar.gz\"\r\n\
Content-Type: application/octet-stream\r\n\r\n\x1f\x8b\r\n--boundary--\r\n",
);
}
#[test]
fn multipart_file_field_escapes_special_characters() {
let mut m = MultipartBuilder::new("boundary");
m.file_field("upload", r#"export "foo\bar".tar.gz"#, b"data");
let body = String::from_utf8(m.finish()).unwrap();
assert!(
body.contains(r#"filename="export \"foo\\bar\".tar.gz""#),
"unexpected body: {body:?}",
);
}
#[test]
fn multipart_content_type() {
let m = MultipartBuilder::new("my-boundary");
assert_eq!(
m.content_type(),
"multipart/form-data; boundary=my-boundary"
);
}
}