use headers::ContentType;
use serde::Serialize;
use utoipa::ToSchema;
use crate::client::error::ApiClientError;
use crate::client::openapi::schema::SchemaEntry;
#[derive(Clone, derive_more::Debug)]
pub struct CallBody {
pub(in crate::client) content_type: ContentType,
pub(in crate::client) entry: SchemaEntry,
#[debug(ignore)]
pub(in crate::client) data: Vec<u8>,
}
impl CallBody {
pub fn json<T>(t: &T) -> Result<Self, ApiClientError>
where
T: Serialize + ToSchema + 'static,
{
let content_type = ContentType::json();
let mut entry = SchemaEntry::of::<T>();
let example = serde_json::to_value(t)?;
entry.add_example(example);
let data = serde_json::to_vec(t)?;
let result = Self {
content_type,
entry,
data,
};
Ok(result)
}
pub fn form<T>(t: &T) -> Result<Self, ApiClientError>
where
T: Serialize + ToSchema + 'static,
{
let content_type = ContentType::form_url_encoded();
let mut entry = SchemaEntry::of::<T>();
let example = serde_json::to_value(t)?;
entry.add_example(example);
let data = serde_urlencoded::to_string(t)
.map_err(|e| ApiClientError::SerializationError {
message: format!("Failed to serialize form data: {e}"),
})?
.into_bytes();
let result = Self {
content_type,
entry,
data,
};
Ok(result)
}
pub fn raw(data: Vec<u8>, content_type: ContentType) -> Self {
let entry = SchemaEntry::raw_binary();
Self {
content_type,
entry,
data,
}
}
pub fn text(text: &str) -> Self {
Self::raw(text.as_bytes().to_vec(), ContentType::text())
}
pub fn multipart(parts: Vec<(&str, &str)>) -> Self {
let boundary = format!("----formdata-clawspec-{}", uuid::Uuid::new_v4());
let content_type = format!("multipart/form-data; boundary={boundary}");
let mut body_data = Vec::new();
for (name, value) in parts {
body_data.extend_from_slice(format!("--{boundary}\r\n").as_bytes());
body_data.extend_from_slice(
format!("Content-Disposition: form-data; name=\"{name}\"\r\n\r\n").as_bytes(),
);
body_data.extend_from_slice(value.as_bytes());
body_data.extend_from_slice(b"\r\n");
}
body_data.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
let content_type = ContentType::from(
content_type
.parse::<mime::Mime>()
.expect("multipart content type format is valid"),
);
let entry = SchemaEntry::raw_binary();
Self {
content_type,
entry,
data: body_data,
}
}
#[cfg(feature = "redaction")]
pub(crate) fn json_without_example<T>(t: &T) -> Result<Self, ApiClientError>
where
T: Serialize + ToSchema + 'static,
{
let content_type = ContentType::json();
let entry = SchemaEntry::of::<T>(); let data = serde_json::to_vec(t)?;
Ok(Self {
content_type,
entry,
data,
})
}
#[cfg(feature = "redaction")]
pub(crate) fn set_example(&mut self, example: serde_json::Value) {
self.entry.add_example(example);
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
struct TestData {
name: String,
value: i32,
}
#[test]
fn test_call_body_json_creates_valid_body() {
let test_data = TestData {
name: "test".to_string(),
value: 42,
};
let body = CallBody::json(&test_data).expect("should create body");
insta::assert_debug_snapshot!(body, @r#"
CallBody {
content_type: ContentType(
"application/json",
),
entry: SchemaEntry {
type_name: "clawspec_core::client::parameters::body::tests::TestData",
name: "TestData",
examples: {
Object {
"name": String("test"),
"value": Number(42),
},
},
..
},
..
}
"#);
let parsed = serde_json::from_slice::<TestData>(&body.data).expect("should parse JSON");
assert_eq!(parsed, test_data);
}
#[test]
fn test_call_body_form_creates_valid_body() {
let test_data = TestData {
name: "test user".to_string(),
value: 42,
};
let body = CallBody::form(&test_data).expect("should create form body");
assert_eq!(body.content_type, headers::ContentType::form_url_encoded());
assert_eq!(body.entry.name, "TestData");
let form_data = String::from_utf8(body.data).expect("should be valid UTF-8");
insta::assert_snapshot!(form_data, @"name=test+user&value=42");
}
#[test]
fn test_call_body_raw_creates_valid_body() {
let binary_data = vec![0xFF, 0xFE, 0xFD, 0xFC];
let content_type = headers::ContentType::octet_stream();
let body = CallBody::raw(binary_data.clone(), content_type.clone());
assert_eq!(body.content_type, content_type);
assert_eq!(body.entry.name, "binary");
assert_eq!(body.data, binary_data);
}
#[test]
fn test_call_body_text_creates_valid_body() {
let text_content = "Hello, World!";
let body = CallBody::text(text_content);
assert_eq!(body.content_type, headers::ContentType::text());
assert_eq!(body.entry.name, "binary");
assert_eq!(body.data, text_content.as_bytes());
}
}