use std::collections::HashMap;
use reqwest::Method;
use serde::Serialize;
use crate::core::{
api_req::ApiRequest,
api_resp::{ApiResponseTrait, BaseResponse},
config::Config,
constants::AccessTokenType,
http::Transport,
req_option::RequestOption,
SDKResult,
};
pub struct RequestExecutor;
impl RequestExecutor {
pub async fn get<T: ApiResponseTrait>(
config: &Config,
path: &str,
supported_tokens: Vec<AccessTokenType>,
query_params: Option<HashMap<&'static str, String>>,
option: Option<RequestOption>,
) -> SDKResult<BaseResponse<T>> {
Self::execute(
config,
Method::GET,
path,
supported_tokens,
query_params,
None::<()>,
option,
)
.await
}
pub async fn post<T: ApiResponseTrait, B: Serialize>(
config: &Config,
path: &str,
supported_tokens: Vec<AccessTokenType>,
body: Option<B>,
option: Option<RequestOption>,
) -> SDKResult<BaseResponse<T>> {
Self::execute(
config,
Method::POST,
path,
supported_tokens,
None,
body,
option,
)
.await
}
pub async fn put<T: ApiResponseTrait, B: Serialize>(
config: &Config,
path: &str,
supported_tokens: Vec<AccessTokenType>,
body: Option<B>,
option: Option<RequestOption>,
) -> SDKResult<BaseResponse<T>> {
Self::execute(
config,
Method::PUT,
path,
supported_tokens,
None,
body,
option,
)
.await
}
pub async fn delete<T: ApiResponseTrait>(
config: &Config,
path: &str,
supported_tokens: Vec<AccessTokenType>,
option: Option<RequestOption>,
) -> SDKResult<BaseResponse<T>> {
Self::execute(
config,
Method::DELETE,
path,
supported_tokens,
None,
None::<()>,
option,
)
.await
}
pub async fn patch<T: ApiResponseTrait, B: Serialize>(
config: &Config,
path: &str,
supported_tokens: Vec<AccessTokenType>,
body: Option<B>,
option: Option<RequestOption>,
) -> SDKResult<BaseResponse<T>> {
Self::execute(
config,
Method::PATCH,
path,
supported_tokens,
None,
body,
option,
)
.await
}
pub async fn execute<T: ApiResponseTrait, B: Serialize>(
config: &Config,
method: Method,
path: &str,
supported_tokens: Vec<AccessTokenType>,
query_params: Option<HashMap<&'static str, String>>,
body: Option<B>,
option: Option<RequestOption>,
) -> SDKResult<BaseResponse<T>> {
let mut api_req = ApiRequest {
http_method: method,
api_path: path.to_string(),
supported_access_token_types: supported_tokens,
..Default::default()
};
if let Some(params) = query_params {
api_req.query_params = params;
}
if let Some(body_data) = body {
api_req.body = serde_json::to_vec(&body_data)
.map_err(|e| crate::core::error::LarkAPIError::DeserializeError(e.to_string()))?;
}
Transport::request(api_req, config, option).await
}
#[allow(clippy::too_many_arguments)]
pub async fn execute_with_path_params<T: ApiResponseTrait, B: Serialize>(
config: &Config,
method: Method,
path_template: &str,
path_params: HashMap<&str, &str>,
supported_tokens: Vec<AccessTokenType>,
query_params: Option<HashMap<&'static str, String>>,
body: Option<B>,
option: Option<RequestOption>,
) -> SDKResult<BaseResponse<T>> {
let mut path = path_template.to_string();
for (key, value) in path_params {
path = path.replace(&format!("{{{key}}}"), value);
}
Self::execute(
config,
method,
&path,
supported_tokens,
query_params,
body,
option,
)
.await
}
pub async fn json_request<T: ApiResponseTrait, B: Serialize>(
config: &Config,
method: Method,
path: &str,
body: &B,
option: Option<RequestOption>,
) -> SDKResult<BaseResponse<T>> {
Self::execute(
config,
method,
path,
vec![AccessTokenType::Tenant, AccessTokenType::User], None,
Some(body),
option,
)
.await
}
pub async fn query_request<T: ApiResponseTrait>(
config: &Config,
path: &str,
query_params: Option<HashMap<&'static str, String>>,
option: Option<RequestOption>,
) -> SDKResult<BaseResponse<T>> {
Self::get(
config,
path,
vec![AccessTokenType::Tenant, AccessTokenType::User], query_params,
option,
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::api_resp::ResponseFormat;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct TestRequest {
message: String,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct TestResponse {
id: String,
status: String,
}
impl ApiResponseTrait for TestResponse {
fn data_format() -> ResponseFormat {
ResponseFormat::Data
}
}
#[tokio::test]
async fn test_request_executor_path_params() {
let path_template = "/open-apis/im/v1/messages/{message_id}/replies/{reply_id}";
let path_params = HashMap::from([("message_id", "om_123"), ("reply_id", "reply_456")]);
let expected_path = "/open-apis/im/v1/messages/om_123/replies/reply_456";
let mut path = path_template.to_string();
for (key, value) in path_params {
path = path.replace(&format!("{{{key}}}"), value);
}
assert_eq!(path, expected_path);
}
#[test]
fn test_request_body_serialization() {
let request = TestRequest {
message: "Hello, World!".to_string(),
};
let serialized = serde_json::to_vec(&request).unwrap();
let expected = br#"{"message":"Hello, World!"}"#;
assert_eq!(serialized, expected);
}
#[test]
fn test_request_executor_get_method() {
use crate::core::constants::AccessTokenType;
use std::collections::HashMap;
let supported_tokens = [AccessTokenType::Tenant, AccessTokenType::User];
let mut query_params = HashMap::new();
query_params.insert("page", "1".to_string());
query_params.insert("limit", "10".to_string());
assert_eq!(supported_tokens.len(), 2);
assert_eq!(query_params.len(), 2);
assert_eq!(query_params.get("page"), Some(&"1".to_string()));
assert_eq!(query_params.get("limit"), Some(&"10".to_string()));
}
#[test]
fn test_request_executor_post_method_signature() {
use crate::core::constants::AccessTokenType;
let supported_tokens = [AccessTokenType::App];
let request_body = TestRequest {
message: "test message".to_string(),
};
let serialized = serde_json::to_vec(&request_body).unwrap();
assert!(!serialized.is_empty());
assert_eq!(supported_tokens.len(), 1);
assert_eq!(supported_tokens[0], AccessTokenType::App);
}
#[test]
fn test_request_executor_put_method_signature() {
use crate::core::constants::AccessTokenType;
let supported_tokens = [AccessTokenType::Tenant];
let request_body = TestRequest {
message: "update message".to_string(),
};
let serialized = serde_json::to_vec(&request_body).unwrap();
assert!(!serialized.is_empty());
assert_eq!(supported_tokens.len(), 1);
assert_eq!(supported_tokens[0], AccessTokenType::Tenant);
}
#[test]
fn test_request_executor_delete_method_signature() {
use crate::core::constants::AccessTokenType;
let supported_tokens = [AccessTokenType::User];
assert_eq!(supported_tokens.len(), 1);
assert_eq!(supported_tokens[0], AccessTokenType::User);
}
#[test]
fn test_request_executor_patch_method_signature() {
use crate::core::constants::AccessTokenType;
let supported_tokens = [AccessTokenType::Tenant, AccessTokenType::App];
let request_body = TestRequest {
message: "patch message".to_string(),
};
let serialized = serde_json::to_vec(&request_body).unwrap();
assert!(!serialized.is_empty());
assert_eq!(supported_tokens.len(), 2);
assert!(supported_tokens.contains(&AccessTokenType::Tenant));
assert!(supported_tokens.contains(&AccessTokenType::App));
}
#[test]
fn test_request_executor_execute_with_path_params_replacement() {
use std::collections::HashMap;
let path_template = "/open-apis/im/v1/messages/{message_id}/reactions/{reaction_id}";
let mut path_params = HashMap::new();
path_params.insert("message_id", "om_test123");
path_params.insert("reaction_id", "react_456");
let mut path = path_template.to_string();
for (key, value) in &path_params {
path = path.replace(&format!("{{{key}}}"), value);
}
let expected = "/open-apis/im/v1/messages/om_test123/reactions/react_456";
assert_eq!(path, expected);
let path_template_missing = "/api/{missing_param}/test";
let empty_params: HashMap<&str, &str> = HashMap::new();
let mut path_missing = path_template_missing.to_string();
for (key, value) in &empty_params {
path_missing = path_missing.replace(&format!("{{{key}}}"), value);
}
assert_eq!(path_missing, "/api/{missing_param}/test");
}
#[test]
fn test_request_executor_execute_with_path_params_edge_cases() {
use std::collections::HashMap;
let empty_path = "";
let mut path_params = HashMap::new();
path_params.insert("param", "value");
let mut path = empty_path.to_string();
for (key, value) in &path_params {
path = path.replace(&format!("{{{key}}}"), value);
}
assert_eq!(path, "");
let duplicate_path = "/api/{id}/sub/{id}/item";
let mut duplicate_params = HashMap::new();
duplicate_params.insert("id", "123");
let mut path_duplicate = duplicate_path.to_string();
for (key, value) in &duplicate_params {
path_duplicate = path_duplicate.replace(&format!("{{{key}}}"), value);
}
assert_eq!(path_duplicate, "/api/123/sub/123/item");
let special_path = "/api/{name}/test";
let mut special_params = HashMap::new();
special_params.insert("name", "test@#$%");
let mut path_special = special_path.to_string();
for (key, value) in &special_params {
path_special = path_special.replace(&format!("{{{key}}}"), value);
}
assert_eq!(path_special, "/api/test@#$%/test");
}
#[test]
fn test_request_executor_json_request_defaults() {
use crate::core::constants::AccessTokenType;
let default_tokens = [AccessTokenType::Tenant, AccessTokenType::User];
assert_eq!(default_tokens.len(), 2);
assert!(default_tokens.contains(&AccessTokenType::Tenant));
assert!(default_tokens.contains(&AccessTokenType::User));
let expected_tokens = [AccessTokenType::Tenant, AccessTokenType::User];
assert_eq!(default_tokens, expected_tokens);
}
#[test]
fn test_request_executor_query_request_defaults() {
use crate::core::constants::AccessTokenType;
let default_tokens = [AccessTokenType::Tenant, AccessTokenType::User];
assert_eq!(default_tokens.len(), 2);
assert!(default_tokens.contains(&AccessTokenType::Tenant));
assert!(default_tokens.contains(&AccessTokenType::User));
}
#[test]
fn test_request_executor_body_serialization_edge_cases() {
use serde_json;
#[derive(serde::Serialize)]
struct EmptyRequest {}
let empty_request = EmptyRequest {};
let serialized = serde_json::to_vec(&empty_request).unwrap();
let expected = b"{}";
assert_eq!(serialized, expected);
#[derive(serde::Serialize)]
struct OptionalRequest {
message: Option<String>,
count: Option<i32>,
}
let optional_request = OptionalRequest {
message: None,
count: Some(42),
};
let serialized = serde_json::to_vec(&optional_request).unwrap();
let deserialized: serde_json::Value = serde_json::from_slice(&serialized).unwrap();
assert!(deserialized.get("message").unwrap().is_null());
assert_eq!(deserialized.get("count").unwrap().as_i64().unwrap(), 42);
}
#[test]
fn test_request_executor_query_params_handling() {
use std::collections::HashMap;
let mut query_params: HashMap<&'static str, String> = HashMap::new();
query_params.insert("page_size", "20".to_string());
query_params.insert("sort_order", "desc".to_string());
query_params.insert("filter", "active".to_string());
assert_eq!(query_params.len(), 3);
assert_eq!(query_params.get("page_size"), Some(&"20".to_string()));
assert_eq!(query_params.get("sort_order"), Some(&"desc".to_string()));
assert_eq!(query_params.get("filter"), Some(&"active".to_string()));
let empty_params: HashMap<&'static str, String> = HashMap::new();
assert!(empty_params.is_empty());
let mut special_params: HashMap<&'static str, String> = HashMap::new();
special_params.insert("search", "hello world".to_string());
special_params.insert("encoded", "test@domain.com".to_string());
assert_eq!(
special_params.get("search"),
Some(&"hello world".to_string())
);
assert_eq!(
special_params.get("encoded"),
Some(&"test@domain.com".to_string())
);
}
#[test]
fn test_request_executor_access_token_types() {
use crate::core::constants::AccessTokenType;
let all_types = [
AccessTokenType::Tenant,
AccessTokenType::User,
AccessTokenType::App,
];
assert_eq!(all_types.len(), 3);
assert_ne!(AccessTokenType::Tenant, AccessTokenType::User);
assert_ne!(AccessTokenType::User, AccessTokenType::App);
assert_ne!(AccessTokenType::App, AccessTokenType::Tenant);
let tenant_user = [AccessTokenType::Tenant, AccessTokenType::User];
let app_only = [AccessTokenType::App];
assert_eq!(tenant_user.len(), 2);
assert_eq!(app_only.len(), 1);
}
#[test]
fn test_request_executor_method_types() {
use reqwest::Method;
let get_method = Method::GET;
let post_method = Method::POST;
let put_method = Method::PUT;
let delete_method = Method::DELETE;
let patch_method = Method::PATCH;
assert_eq!(get_method.as_str(), "GET");
assert_eq!(post_method.as_str(), "POST");
assert_eq!(put_method.as_str(), "PUT");
assert_eq!(delete_method.as_str(), "DELETE");
assert_eq!(patch_method.as_str(), "PATCH");
assert_ne!(get_method, post_method);
assert_ne!(post_method, put_method);
assert_ne!(put_method, delete_method);
assert_ne!(delete_method, patch_method);
}
#[test]
fn test_request_executor_api_request_construction() {
use crate::core::api_req::ApiRequest;
use crate::core::constants::AccessTokenType;
use reqwest::Method;
use std::collections::HashMap;
let mut api_req = ApiRequest {
http_method: Method::POST,
api_path: "/test/path".to_string(),
supported_access_token_types: vec![AccessTokenType::Tenant],
..Default::default()
};
assert_eq!(api_req.http_method, Method::POST);
assert_eq!(api_req.api_path, "/test/path");
assert_eq!(api_req.supported_access_token_types.len(), 1);
assert_eq!(
api_req.supported_access_token_types[0],
AccessTokenType::Tenant
);
let mut query_params = HashMap::new();
query_params.insert("key", "value".to_string());
api_req.query_params = query_params;
assert_eq!(api_req.query_params.len(), 1);
assert_eq!(api_req.query_params.get("key"), Some(&"value".to_string()));
let body_data = TestRequest {
message: "test".to_string(),
};
api_req.body = serde_json::to_vec(&body_data).unwrap();
assert!(!api_req.body.is_empty());
let deserialized: TestRequest = serde_json::from_slice(&api_req.body).unwrap();
assert_eq!(deserialized.message, "test");
}
#[test]
fn test_request_executor_error_handling() {
use serde_json;
#[derive(serde::Serialize)]
struct ValidRequest {
message: String,
}
let valid_request = ValidRequest {
message: "test".to_string(),
};
let serialization_result = serde_json::to_vec(&valid_request);
assert!(serialization_result.is_ok());
let invalid_json = "invalid json";
let parse_result: Result<serde_json::Value, _> = serde_json::from_str(invalid_json);
assert!(parse_result.is_err());
}
#[test]
fn test_request_option_integration() {
use crate::core::req_option::RequestOption;
let option = RequestOption::builder()
.tenant_key("test_tenant")
.user_access_token("user_token")
.request_id("req_123")
.add_header("X-Test-Header", "test_value")
.build();
assert_eq!(option.tenant_key, "test_tenant");
assert_eq!(option.user_access_token, "user_token");
assert_eq!(option.request_id, "req_123");
assert_eq!(
option.header.get("X-Test-Header"),
Some(&"test_value".to_string())
);
let none_option: Option<RequestOption> = None;
assert!(none_option.is_none());
let some_option = Some(option);
assert!(some_option.is_some());
}
#[test]
fn test_request_executor_const_values() {
let path_templates = vec![
"/open-apis/im/v1/messages",
"/open-apis/im/v1/messages/{message_id}",
"/open-apis/contact/v3/users",
"/open-apis/contact/v3/users/{user_id}",
];
for template in path_templates {
assert!(!template.is_empty());
assert!(template.starts_with("/open-apis"));
}
let query_param_names = vec![
"page_token",
"page_size",
"user_id_type",
"container_id",
"sort_order",
];
for param_name in query_param_names {
assert!(!param_name.is_empty());
assert!(param_name.is_ascii());
}
}
}