openlark-docs 0.19.0

飞书开放平台云文档服务模块 - 文档、表格、知识库API (202 APIs, 100% 覆盖,不含旧版本)
Documentation
//! 新建文件夹
//!
//! 在用户云空间的指定文件夹中创建一个新的空文件夹。
//!
//! docPath: <https://open.feishu.cn/document/server-docs/docs/drive-v1/folder/create_folder>

use openlark_core::{
    SDKResult,
    api::{ApiResponseTrait, ResponseFormat},
    config::Config,
    http::Transport,
};

use crate::common::{api_endpoints::DriveApi, api_utils::*};
use serde::{Deserialize, Serialize};

/// 新建文件夹请求
///
/// 用于在用户云空间的指定文件夹中创建一个新的空文件夹。
///
/// # 字段说明
///
/// - `name`: 文件夹名称,长度必须在 1~256 字节之间
/// - `folder_token`: 父文件夹的 token。参数为空字符串时,表示在根目录下创建文件夹
///
/// # 示例
///
/// ```rust,ignore
/// use openlark_core::config::Config;
/// use openlark_docs::ccm::drive::v1::file::CreateFolderRequest;
///
/// let config = Config::builder().app_id("app_id").app_secret("app_secret").build();
/// let request = CreateFolderRequest::new(config, "新建文件夹", "parent_token");
/// let response = request.execute().await?;
/// println!("新建文件夹 token: {}", response.token);
/// ```
#[derive(Debug, Clone, Serialize)]
pub struct CreateFolderRequest {
    #[serde(skip)]
    config: Config,
    /// 文件夹名称
    pub name: String,
    /// 父文件夹的 token。参数为空字符串时,表示在根目录下创建文件夹
    pub folder_token: String,
}

/// 新建文件夹响应
///
/// 包含新建文件夹的基本信息。
///
/// # 字段说明
///
/// - `token`: 新建的文件夹的唯一标识符
/// - `url`: 新建的文件夹的 URL 链接,可直接在浏览器中打开
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateFolderResponse {
    /// 新建的文件夹的 token
    pub token: String,
    /// 新建的文件夹的 URL 链接
    pub url: String,
}

impl ApiResponseTrait for CreateFolderResponse {
    fn data_format() -> ResponseFormat {
        ResponseFormat::Data
    }
}

impl CreateFolderRequest {
    /// 创建新建文件夹请求
    ///
    /// # 参数
    ///
    /// - `config`: SDK 配置实例
    /// - `name`: 文件夹名称,长度必须在 1~256 字节之间
    /// - `folder_token`: 父文件夹 token,空字符串表示根目录
    ///
    /// # 示例
    ///
    /// ```rust,ignore
    /// let request = CreateFolderRequest::new(config, "我的文件夹", "parent_token");
    /// ```
    pub fn new(config: Config, name: impl Into<String>, folder_token: impl Into<String>) -> Self {
        Self {
            config,
            name: name.into(),
            folder_token: folder_token.into(),
        }
    }

    /// 执行请求(使用默认选项)
    ///
    /// 发送新建文件夹请求到飞书服务器。
    ///
    /// # 返回
    ///
    /// 返回包含新建文件夹 token 和 URL 的响应
    ///
    /// # 错误
    ///
    /// - 名称长度不在 1~256 字节之间时返回校验错误
    /// - 网络错误、认证失败等返回相应的错误类型
    pub async fn execute(self) -> SDKResult<CreateFolderResponse> {
        self.execute_with_options(openlark_core::req_option::RequestOption::default())
            .await
    }

    /// 使用自定义选项执行请求
    ///
    /// # 参数
    ///
    /// - `option`: 请求选项,可用于设置超时、重试策略等
    pub async fn execute_with_options(
        self,
        option: openlark_core::req_option::RequestOption,
    ) -> SDKResult<CreateFolderResponse> {
        // 验证文件夹名称长度
        let name_len = self.name.len();
        if name_len == 0 || name_len > 256 {
            return Err(openlark_core::error::validation_error(
                "name",
                "name 长度必须在 1~256 字节之间",
            ));
        }

        let api_endpoint = DriveApi::CreateFolder;
        let request = api_endpoint
            .to_request::<CreateFolderResponse>()
            .body(serialize_params(&self, "新建文件夹")?);

        Transport::request_typed(request, &self.config, Some(option), "新建文件夹").await
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_create_folder_request() {
        let config = Config::default();
        let request = CreateFolderRequest::new(config, "test_folder", "parent_token");
        assert_eq!(request.name, "test_folder");
        assert_eq!(request.folder_token, "parent_token");
    }

    #[test]
    fn test_create_folder_with_empty_name() {
        let config = Config::default();
        let request = CreateFolderRequest::new(config, "", "parent_token");
        // 空名称应在 execute_with_options 时被校验拦截
        // 此测试验证请求结构可以构建,但执行时会失败
        assert_eq!(request.name, "");
    }

    #[test]
    fn test_create_folder_with_too_long_name() {
        let config = Config::default();
        let long_name = "a".repeat(257); // 超过256字节
        let request = CreateFolderRequest::new(config, long_name, "parent_token");
        // 过长名称应在 execute_with_options 时被校验拦截
        assert!(request.name.len() > 256);
    }

    #[test]
    fn test_create_folder_with_max_length_name() {
        let config = Config::default();
        // UTF-8 中每个中文字符占 3 字节,256 字节约 85 个中文字符
        let max_name = "a".repeat(256); // ASCII 字符,1 字节一个
        let request = CreateFolderRequest::new(config, max_name.clone(), "parent_token");
        assert_eq!(request.name, max_name);
        assert_eq!(request.name.len(), 256);
    }

    #[test]
    fn test_create_folder_in_root() {
        let config = Config::default();
        // 空字符串表示根目录
        let request = CreateFolderRequest::new(config, "test_folder", "");
        assert_eq!(request.folder_token, "");
    }

    /// 端到端:使用 catalog 语义,POST /open-apis/drive/v1/files/create_folder ,断言 method/path/auth。
    #[tokio::test]
    async fn test_create_folder_returns_data_on_success_440() {
        use crate::common::test_utils::tenant_test_transport;
        use serde_json::json;
        use wiremock::matchers::{header, method, path};
        use wiremock::{Mock, ResponseTemplate};

        let (server, config, option) = tenant_test_transport().await;
        Mock::given(method("POST"))
            .and(path("/open-apis/drive/v1/files/create_folder"))
            .and(header("Authorization", "Bearer test-tenant-token"))
            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
                "code": 0,
                "msg": "success",
                "data": { "token": "new_folder_token", "url": "https://example.com/folder" }
            })))
            .mount(&server)
            .await;

        let resp = CreateFolderRequest::new(config, "测试文件夹", "parent_token_001")
            .execute_with_options(option)
            .await
            .expect("新建文件夹应成功");
        assert_eq!(resp.token, "new_folder_token");

        let received = server.received_requests().await.unwrap_or_default();
        assert_eq!(received.len(), 1);
        assert_eq!(received[0].method, "POST");
        assert_eq!(
            received[0].url.path(),
            "/open-apis/drive/v1/files/create_folder"
        );
        assert_eq!(
            received[0]
                .headers
                .get("authorization")
                .and_then(|h| h.to_str().ok()),
            Some("Bearer test-tenant-token")
        );
        let body: serde_json::Value =
            serde_json::from_slice(&received[0].body).expect("请求体应为合法 JSON");
        assert_eq!(body["name"], "测试文件夹");
        assert_eq!(body["folder_token"], "parent_token_001");
    }

    #[test]
    fn test_create_folder_url() {
        let ep = DriveApi::CreateFolder;
        assert!(ep.to_url().contains("create_folder"));
    }
}