use log::debug;
use serde_json::Value;
use tracing::{info_span, Instrument};
use crate::core::{
api_resp::{ApiResponseTrait, BaseResponse, RawResponse, ResponseFormat},
error::LarkAPIError,
observability::ResponseTracker,
SDKResult,
};
pub struct ImprovedResponseHandler;
impl ImprovedResponseHandler {
pub async fn handle_response<T: ApiResponseTrait>(
response: reqwest::Response,
) -> SDKResult<BaseResponse<T>> {
let format = match T::data_format() {
ResponseFormat::Data => "data",
ResponseFormat::Flatten => "flatten",
ResponseFormat::Binary => "binary",
};
let span = info_span!(
"response_handling",
format = format,
status_code = response.status().as_u16(),
content_length = tracing::field::Empty,
processing_duration_ms = tracing::field::Empty,
);
async move {
let start_time = std::time::Instant::now();
let content_length = response.content_length();
if let Some(length) = content_length {
tracing::Span::current().record("content_length", length);
}
let result = match T::data_format() {
ResponseFormat::Data => Self::handle_data_response(response).await,
ResponseFormat::Flatten => Self::handle_flatten_response(response).await,
ResponseFormat::Binary => Self::handle_binary_response(response).await,
};
let duration_ms = start_time.elapsed().as_millis() as u64;
tracing::Span::current().record("processing_duration_ms", duration_ms);
result
}
.instrument(span)
.await
}
async fn handle_data_response<T: ApiResponseTrait>(
response: reqwest::Response,
) -> SDKResult<BaseResponse<T>> {
let tracker = ResponseTracker::start("json_data", response.content_length());
let response_text = response.text().await?;
debug!("Raw response: {response_text}");
tracker.parsing_complete();
match serde_json::from_str::<BaseResponse<T>>(&response_text) {
Ok(base_response) => {
tracker.success();
Ok(base_response)
}
Err(direct_parse_err) => {
tracing::debug!("Direct parsing failed, attempting structured data extraction");
match serde_json::from_str::<Value>(&response_text) {
Ok(raw_value) => {
let code = raw_value["code"].as_i64().unwrap_or(-1) as i32;
let msg = raw_value["msg"]
.as_str()
.unwrap_or("Unknown error")
.to_string();
let data = if code == 0 {
if let Some(data_value) = raw_value.get("data") {
match serde_json::from_value::<T>(data_value.clone()) {
Ok(parsed_data) => {
tracing::debug!("Successfully parsed data field as type T");
Some(parsed_data)
}
Err(data_parse_err) => {
tracing::debug!("Failed to parse data field as type T: {data_parse_err:?}");
if std::any::type_name::<T>().contains("CreateMessageResp")
{
let wrapped_value = serde_json::json!({
"data": data_value
});
match serde_json::from_value::<T>(wrapped_value) {
Ok(wrapped_data) => {
tracing::debug!("Successfully parsed data by wrapping Message in CreateMessageResp");
Some(wrapped_data)
}
Err(_) => {
tracing::warn!("Failed to parse even after wrapping, but response contains valid message data");
None
}
}
} else {
None
}
}
}
} else {
tracing::debug!("No data field found in successful response");
None
}
} else {
None
};
tracker.validation_complete();
tracker.success();
Ok(BaseResponse {
raw_response: RawResponse {
code,
msg,
err: None,
},
data,
})
}
Err(fallback_err) => {
let error_msg = format!(
"Failed to parse response. Direct parse error: {}. Fallback parse error: {}",
direct_parse_err, fallback_err
);
tracker.error(&error_msg);
Err(LarkAPIError::IllegalParamError(error_msg))
}
}
}
}
}
async fn handle_flatten_response<T: ApiResponseTrait>(
response: reqwest::Response,
) -> SDKResult<BaseResponse<T>> {
let tracker = ResponseTracker::start("json_flatten", response.content_length());
let response_text = response.text().await?;
debug!("Raw response: {response_text}");
let raw_value: Value = match serde_json::from_str(&response_text) {
Ok(value) => {
tracker.parsing_complete();
value
}
Err(e) => {
let error_msg = format!("Failed to parse JSON: {}", e);
tracker.error(&error_msg);
return Err(LarkAPIError::IllegalParamError(error_msg));
}
};
let raw_response: RawResponse = match serde_json::from_value(raw_value.clone()) {
Ok(response) => response,
Err(e) => {
let error_msg = format!("Failed to parse raw response: {}", e);
tracker.error(&error_msg);
return Err(LarkAPIError::IllegalParamError(error_msg));
}
};
let data = if raw_response.code == 0 {
match serde_json::from_value::<T>(raw_value) {
Ok(parsed_data) => {
tracker.validation_complete();
Some(parsed_data)
}
Err(e) => {
debug!("Failed to parse data for flatten response: {e}");
tracker.validation_complete();
None
}
}
} else {
tracker.validation_complete();
None
};
tracker.success();
Ok(BaseResponse { raw_response, data })
}
async fn handle_binary_response<T: ApiResponseTrait>(
response: reqwest::Response,
) -> SDKResult<BaseResponse<T>> {
let tracker = ResponseTracker::start("binary", response.content_length());
let file_name = response
.headers()
.get("Content-Disposition")
.and_then(|header| header.to_str().ok())
.and_then(Self::extract_filename)
.unwrap_or_default();
tracker.parsing_complete();
let bytes = match response.bytes().await {
Ok(bytes) => {
let byte_vec = bytes.to_vec();
tracing::debug!("Binary response received: {} bytes", byte_vec.len());
byte_vec
}
Err(e) => {
let error_msg = format!("Failed to read binary response: {}", e);
tracker.error(&error_msg);
return Err(LarkAPIError::RequestError(error_msg));
}
};
let data = match T::from_binary(file_name.clone(), bytes) {
Some(binary_data) => {
tracker.validation_complete();
Some(binary_data)
}
None => {
tracker.validation_complete();
tracing::warn!("Binary data could not be processed for file: {}", file_name);
None
}
};
tracker.success();
Ok(BaseResponse {
raw_response: RawResponse {
code: 0,
msg: "success".to_string(),
err: None,
},
data,
})
}
fn extract_filename(content_disposition: &str) -> Option<String> {
for part in content_disposition.split(';') {
let part = part.trim();
if let Some(filename) = part.strip_prefix("filename*=UTF-8''") {
return Some(filename.to_string());
}
if let Some(filename) = part.strip_prefix("filename=") {
let filename = filename.trim_matches('"');
return Some(filename.to_string());
}
}
None
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct OptimizedBaseResponse<T>
where
T: Default,
{
pub code: i32,
pub msg: String,
#[serde(rename = "error", default, skip_serializing_if = "Option::is_none")]
pub error: Option<ErrorInfo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<T>,
}
impl<T> OptimizedBaseResponse<T>
where
T: Default,
{
pub fn is_success(&self) -> bool {
self.code == 0
}
pub fn into_data(self) -> Result<T, LarkAPIError> {
if self.is_success() {
self.data.ok_or_else(|| {
LarkAPIError::illegal_param("Response is successful but data is missing")
})
} else {
Err(LarkAPIError::api_error(
self.code, self.msg, None, ))
}
}
pub fn data(&self) -> Option<&T> {
self.data.as_ref()
}
pub fn has_error(&self) -> bool {
self.error.is_some()
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct ErrorInfo {
#[serde(rename = "key", default, skip_serializing_if = "Option::is_none")]
pub log_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub details: Vec<ErrorDetail>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct ErrorDetail {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[macro_export]
macro_rules! impl_api_response {
($type:ty, $format:expr) => {
impl ApiResponseTrait for $type {
fn data_format() -> ResponseFormat {
$format
}
}
};
($type:ty, $format:expr, binary) => {
impl ApiResponseTrait for $type {
fn data_format() -> ResponseFormat {
$format
}
fn from_binary(file_name: String, body: Vec<u8>) -> Option<Self> {
Some(<$type>::from_binary_data(file_name, body))
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::api_resp::ResponseFormat;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq, Default, Clone)]
struct TestData {
id: i32,
name: String,
}
impl ApiResponseTrait for TestData {
fn data_format() -> ResponseFormat {
ResponseFormat::Data
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Default, Clone)]
struct TestFlattenData {
id: i32,
name: String,
code: i32,
msg: String,
}
impl ApiResponseTrait for TestFlattenData {
fn data_format() -> ResponseFormat {
ResponseFormat::Flatten
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Default, Clone)]
struct TestBinaryData {
file_name: String,
content: Vec<u8>,
}
impl ApiResponseTrait for TestBinaryData {
fn data_format() -> ResponseFormat {
ResponseFormat::Binary
}
fn from_binary(file_name: String, body: Vec<u8>) -> Option<Self> {
Some(TestBinaryData {
file_name,
content: body,
})
}
}
#[test]
fn test_optimized_base_response_success() {
let response = OptimizedBaseResponse {
code: 0,
msg: "success".to_string(),
error: None,
data: Some(TestData {
id: 1,
name: "test".to_string(),
}),
};
assert!(response.is_success());
assert!(response.data().is_some());
assert_eq!(response.data().unwrap().id, 1);
assert!(!response.has_error());
}
#[test]
fn test_optimized_base_response_error() {
let response: OptimizedBaseResponse<TestData> = OptimizedBaseResponse {
code: 400,
msg: "Bad Request".to_string(),
error: Some(ErrorInfo {
log_id: Some("log123".to_string()),
details: vec![],
}),
data: None,
};
assert!(!response.is_success());
assert!(response.has_error());
assert!(response.data().is_none());
}
#[test]
fn test_optimized_base_response_into_data_success() {
let response = OptimizedBaseResponse {
code: 0,
msg: "success".to_string(),
error: None,
data: Some(TestData {
id: 1,
name: "test".to_string(),
}),
};
let data = response.into_data().unwrap();
assert_eq!(data.id, 1);
assert_eq!(data.name, "test");
}
#[test]
fn test_optimized_base_response_into_data_error() {
let response: OptimizedBaseResponse<TestData> = OptimizedBaseResponse {
code: 400,
msg: "Bad Request".to_string(),
error: None,
data: None,
};
let result = response.into_data();
assert!(result.is_err());
match result.unwrap_err() {
LarkAPIError::ApiError { code, message, .. } => {
assert_eq!(code, 400);
assert_eq!(message, "Bad Request");
}
_ => panic!("Expected ApiError"),
}
}
#[test]
fn test_optimized_base_response_into_data_success_but_no_data() {
let response: OptimizedBaseResponse<TestData> = OptimizedBaseResponse {
code: 0,
msg: "success".to_string(),
error: None,
data: None,
};
let result = response.into_data();
assert!(result.is_err());
match result.unwrap_err() {
LarkAPIError::IllegalParamError(msg) => {
assert!(msg.contains("data is missing"));
}
_ => panic!("Expected IllegalParamError"),
}
}
#[test]
fn test_error_info_serialization() {
let error_info = ErrorInfo {
log_id: Some("test_log_id".to_string()),
details: vec![
ErrorDetail {
key: Some("field1".to_string()),
value: Some("invalid_value".to_string()),
description: Some("Field is required".to_string()),
},
ErrorDetail {
key: Some("field2".to_string()),
value: None,
description: Some("Missing field".to_string()),
},
],
};
let json = serde_json::to_string(&error_info).unwrap();
let deserialized: ErrorInfo = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.log_id, error_info.log_id);
assert_eq!(deserialized.details.len(), 2);
assert_eq!(deserialized.details[0].key, Some("field1".to_string()));
assert_eq!(deserialized.details[1].value, None);
}
#[test]
fn test_error_detail_optional_fields() {
let detail = ErrorDetail {
key: None,
value: Some("test_value".to_string()),
description: None,
};
let json = serde_json::to_string(&detail).unwrap();
let deserialized: ErrorDetail = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.key, None);
assert_eq!(deserialized.value, Some("test_value".to_string()));
assert_eq!(deserialized.description, None);
}
#[test]
fn test_filename_extraction() {
let cases = vec![
(
"attachment; filename=\"test.txt\"",
Some("test.txt".to_string()),
),
(
"attachment; filename*=UTF-8''test%20file.pdf",
Some("test%20file.pdf".to_string()),
),
(
"attachment; filename=simple.doc",
Some("simple.doc".to_string()),
),
("attachment", None),
("", None),
("filename=\"quoted.txt\"", Some("quoted.txt".to_string())),
("filename=unquoted.txt", Some("unquoted.txt".to_string())),
(
"filename*=UTF-8''unicode%E2%9C%93.txt",
Some("unicode%E2%9C%93.txt".to_string()),
),
(
"attachment; filename=\"spaced file.doc\"; other=value",
Some("spaced file.doc".to_string()),
),
];
for (input, expected) in cases {
let result = ImprovedResponseHandler::extract_filename(input);
assert_eq!(result, expected, "Failed for input: {input}");
}
}
#[test]
fn test_filename_extraction_edge_cases() {
assert_eq!(ImprovedResponseHandler::extract_filename(""), None);
assert_eq!(ImprovedResponseHandler::extract_filename(" "), None);
assert_eq!(ImprovedResponseHandler::extract_filename(";;;"), None);
assert_eq!(
ImprovedResponseHandler::extract_filename("filename="),
Some("".to_string())
);
assert_eq!(
ImprovedResponseHandler::extract_filename("filename*="),
None
); assert_eq!(
ImprovedResponseHandler::extract_filename("filename=\""),
Some("".to_string())
);
assert_eq!(
ImprovedResponseHandler::extract_filename("filename=\"\""),
Some("".to_string())
);
let multi_filename = "filename=\"first.txt\"; filename=\"second.txt\"";
assert_eq!(
ImprovedResponseHandler::extract_filename(multi_filename),
Some("first.txt".to_string())
);
}
#[test]
fn test_json_parsing_performance() {
let json_data = r#"{"code": 0, "msg": "success", "data": {"id": 1, "name": "test"}}"#;
let start = std::time::Instant::now();
let _result: Result<OptimizedBaseResponse<TestData>, _> = serde_json::from_str(json_data);
let direct_parse_time = start.elapsed();
let start = std::time::Instant::now();
let _value: Value = serde_json::from_str(json_data).unwrap();
let _result: Result<OptimizedBaseResponse<TestData>, _> = serde_json::from_value(_value);
let double_parse_time = start.elapsed();
println!("Direct parse time: {direct_parse_time:?}");
println!("Double parse time: {double_parse_time:?}");
}
#[test]
fn test_api_response_trait_data_format() {
assert_eq!(TestData::data_format(), ResponseFormat::Data);
assert_eq!(TestFlattenData::data_format(), ResponseFormat::Flatten);
assert_eq!(TestBinaryData::data_format(), ResponseFormat::Binary);
}
#[test]
fn test_api_response_trait_from_binary() {
let file_name = "test.txt".to_string();
let content = b"Hello, World!".to_vec();
let binary_data = TestBinaryData::from_binary(file_name.clone(), content.clone()).unwrap();
assert_eq!(binary_data.file_name, file_name);
assert_eq!(binary_data.content, content);
let default_result = TestData::from_binary("test.txt".to_string(), vec![1, 2, 3]);
assert!(default_result.is_none());
}
#[tokio::test]
async fn test_handle_data_response_parsing_logic() {
let test_cases = vec![
(r#"{"code": 400, "msg": "Bad Request"}"#, true),
(r#"{"invalid": json"#, false),
];
for (json, should_succeed) in test_cases {
if json.contains("code") && !json.contains("raw_response") {
let fallback_result = serde_json::from_str::<Value>(json);
if should_succeed {
assert!(
fallback_result.is_ok(),
"Fallback parsing should succeed for: {}",
json
);
let value = fallback_result.unwrap();
assert!(value["code"].is_i64());
assert!(value["msg"].is_string());
}
} else if json.contains("invalid") {
let parse_result = serde_json::from_str::<Value>(json);
assert!(parse_result.is_err(), "Invalid JSON should fail to parse");
}
}
}
#[tokio::test]
async fn test_handle_flatten_response_parsing_logic() {
let test_cases = vec![
(
r#"{"id": 1, "name": "test", "code": 0, "msg": "success"}"#,
0,
true,
),
(r#"{"code": 400, "msg": "Bad Request"}"#, 400, false),
(r#"{"invalid": json"#, -1, false),
];
for (json, expected_code, should_have_data) in test_cases {
if json.contains("invalid") {
let parse_result = serde_json::from_str::<Value>(json);
assert!(parse_result.is_err(), "Invalid JSON should fail to parse");
continue;
}
let value_result = serde_json::from_str::<Value>(json);
assert!(value_result.is_ok(), "Valid JSON should parse as Value");
let value = value_result.unwrap();
let raw_response_result = serde_json::from_value::<RawResponse>(value.clone());
if expected_code >= 0 {
assert!(
raw_response_result.is_ok(),
"Should parse RawResponse for: {}",
json
);
let raw_response = raw_response_result.unwrap();
assert_eq!(raw_response.code, expected_code);
if should_have_data && raw_response.code == 0 {
let data_result = serde_json::from_value::<TestFlattenData>(value);
assert!(
data_result.is_ok(),
"Should parse data for success response"
);
}
}
}
}
#[test]
fn test_response_format_display_logic() {
let formats = vec![
(ResponseFormat::Data, "data"),
(ResponseFormat::Flatten, "flatten"),
(ResponseFormat::Binary, "binary"),
];
for (format, expected_str) in formats {
let format_str = match format {
ResponseFormat::Data => "data",
ResponseFormat::Flatten => "flatten",
ResponseFormat::Binary => "binary",
};
assert_eq!(format_str, expected_str);
}
}
#[test]
fn test_binary_response_logic() {
let test_file_name = "test_document.pdf";
let test_content = b"PDF content here".to_vec();
let binary_data =
TestBinaryData::from_binary(test_file_name.to_string(), test_content.clone());
assert!(binary_data.is_some());
let data = binary_data.unwrap();
assert_eq!(data.file_name, test_file_name);
assert_eq!(data.content, test_content);
let empty_data = TestBinaryData::from_binary("empty.txt".to_string(), vec![]);
assert!(empty_data.is_some());
assert_eq!(empty_data.unwrap().content.len(), 0);
}
#[test]
fn test_optimized_response_serialization_roundtrip() {
let original = OptimizedBaseResponse {
code: 0,
msg: "success".to_string(),
error: Some(ErrorInfo {
log_id: Some("test123".to_string()),
details: vec![ErrorDetail {
key: Some("validation".to_string()),
value: Some("failed".to_string()),
description: Some("Field validation failed".to_string()),
}],
}),
data: Some(TestData {
id: 42,
name: "serialization_test".to_string(),
}),
};
let json = serde_json::to_string(&original).unwrap();
let deserialized: OptimizedBaseResponse<TestData> = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.code, original.code);
assert_eq!(deserialized.msg, original.msg);
assert_eq!(deserialized.data, original.data);
assert!(deserialized.error.is_some());
let error = deserialized.error.unwrap();
assert_eq!(error.log_id, Some("test123".to_string()));
assert_eq!(error.details.len(), 1);
assert_eq!(error.details[0].key, Some("validation".to_string()));
}
#[test]
fn test_optimized_response_skipped_fields() {
let response: OptimizedBaseResponse<TestData> = OptimizedBaseResponse {
code: 0,
msg: "success".to_string(),
error: None,
data: None,
};
let json = serde_json::to_string(&response).unwrap();
assert!(!json.contains("\"error\""));
assert!(!json.contains("\"data\""));
assert!(json.contains("\"code\":0"));
assert!(json.contains("\"msg\":\"success\""));
}
#[test]
fn test_macro_api_response_implementation() {
#[derive(Debug, Default, Serialize, Deserialize)]
struct MacroTestData;
impl ApiResponseTrait for MacroTestData {
fn data_format() -> ResponseFormat {
ResponseFormat::Data
}
}
assert_eq!(MacroTestData::data_format(), ResponseFormat::Data);
assert!(MacroTestData::from_binary("test".to_string(), vec![1, 2, 3]).is_none());
}
#[test]
fn test_error_detail_empty_values() {
let detail = ErrorDetail {
key: Some("".to_string()),
value: Some("".to_string()),
description: Some("".to_string()),
};
let json = serde_json::to_string(&detail).unwrap();
let deserialized: ErrorDetail = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.key, Some("".to_string()));
assert_eq!(deserialized.value, Some("".to_string()));
assert_eq!(deserialized.description, Some("".to_string()));
}
#[test]
fn test_content_disposition_header_edge_cases() {
let edge_cases = vec![
("FILENAME=\"test.txt\"", None), ("Filename=\"test.txt\"", None), (
"attachment; filename=\"test.txt\"",
Some("test.txt".to_string()),
),
("attachment; filename = \"test.txt\"", None), (
"attachment; filename=\"test-file_v1.2.txt\"",
Some("test-file_v1.2.txt".to_string()),
),
(
"attachment; filename=\"测试文件.txt\"",
Some("测试文件.txt".to_string()),
),
(
"attachment; filename=\"test.txt\"; filename*=UTF-8''better.txt",
Some("test.txt".to_string()),
),
];
for (input, expected) in edge_cases {
let result = ImprovedResponseHandler::extract_filename(input);
assert_eq!(result, expected, "Failed for input: {}", input);
}
}
}
mod usage_examples {}