use std::borrow::Cow;
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct InputFileRef {
pub attach_name: Option<String>,
pub bytes: Vec<u8>,
pub mime_type: Option<String>,
pub file_name: Option<String>,
}
impl InputFileRef {
pub fn direct(bytes: Vec<u8>) -> Self {
Self {
attach_name: None,
bytes,
mime_type: None,
file_name: None,
}
}
pub fn with_attach_name(attach_name: impl Into<String>, bytes: Vec<u8>) -> Self {
Self {
attach_name: Some(attach_name.into()),
bytes,
mime_type: None,
file_name: None,
}
}
pub fn attach_uri(&self) -> Option<String> {
self.attach_name.as_deref().map(|n| format!("attach://{n}"))
}
pub fn effective_mime(&self) -> &str {
self.mime_type
.as_deref()
.unwrap_or("application/octet-stream")
}
}
#[derive(Debug, Clone)]
pub struct RequestParameter {
pub name: Cow<'static, str>,
pub value: Option<Value>,
pub input_files: Option<Vec<InputFileRef>>,
}
impl RequestParameter {
pub fn new(name: impl Into<Cow<'static, str>>, value: impl Into<Value>) -> Self {
Self {
name: name.into(),
value: Some(value.into()),
input_files: None,
}
}
pub fn with_files(
name: impl Into<Cow<'static, str>>,
value: impl Into<Value>,
files: Vec<InputFileRef>,
) -> Self {
Self {
name: name.into(),
value: Some(value.into()),
input_files: Some(files),
}
}
pub fn file_only(name: impl Into<Cow<'static, str>>, file: InputFileRef) -> Self {
Self {
name: name.into(),
value: None,
input_files: Some(vec![file]),
}
}
pub fn json_value(&self) -> Option<String> {
match &self.value {
None => None,
Some(Value::String(s)) => Some(s.clone()),
Some(v) => Some(v.to_string()),
}
}
pub fn multipart_data(&self) -> Option<Vec<(String, &InputFileRef)>> {
let files = self.input_files.as_ref()?;
let parts: Vec<(String, &InputFileRef)> = files
.iter()
.map(|f| {
let part_name = f
.attach_name
.clone()
.unwrap_or_else(|| self.name.as_ref().to_owned());
(part_name, f)
})
.collect();
Some(parts)
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn json_value_string_not_double_encoded() {
let p = RequestParameter::new("text", json!("hello world"));
assert_eq!(p.json_value().unwrap(), "hello world");
}
#[test]
fn json_value_integer() {
let p = RequestParameter::new("chat_id", json!(99));
assert_eq!(p.json_value().unwrap(), "99");
}
#[test]
fn json_value_bool() {
let p = RequestParameter::new("disable_notification", json!(false));
assert_eq!(p.json_value().unwrap(), "false");
}
#[test]
fn json_value_none_when_file_only() {
let file = InputFileRef::direct(vec![0u8, 1, 2]);
let p = RequestParameter::file_only("photo", file);
assert!(p.json_value().is_none());
}
#[test]
fn json_value_object() {
let p = RequestParameter::new("reply_markup", json!({"inline_keyboard": []}));
let s = p.json_value().unwrap();
let reparsed: serde_json::Value = serde_json::from_str(&s).unwrap();
assert_eq!(reparsed["inline_keyboard"], json!([]));
}
#[test]
fn multipart_data_none_for_plain_param() {
let p = RequestParameter::new("chat_id", json!(1));
assert!(p.multipart_data().is_none());
}
#[test]
fn multipart_data_uses_attach_name() {
let file = InputFileRef::with_attach_name("my_file", vec![0xff]);
let p = RequestParameter::with_files("document", json!("attach://my_file"), vec![file]);
let parts = p.multipart_data().unwrap();
assert_eq!(parts.len(), 1);
assert_eq!(parts[0].0, "my_file");
}
#[test]
fn multipart_data_falls_back_to_param_name() {
let file = InputFileRef::direct(vec![0xde, 0xad]);
let p = RequestParameter::file_only("photo", file);
let parts = p.multipart_data().unwrap();
assert_eq!(parts.len(), 1);
assert_eq!(parts[0].0, "photo");
}
#[test]
fn attach_uri_present_when_name_set() {
let file = InputFileRef::with_attach_name("vid", vec![]);
assert_eq!(file.attach_uri().unwrap(), "attach://vid");
}
#[test]
fn attach_uri_none_for_direct_file() {
let file = InputFileRef::direct(vec![]);
assert!(file.attach_uri().is_none());
}
}