diff --git a/CHANGELOG.md b/CHANGELOG.md index 05c935fbe..cc59a9032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking +- **meeting_room 17 叶 `execute()` 返回类型 `Value` → typed Response**(#349): + `meeting_room/{building,room,country,district,freebusy,instance,summary}` 全部 + `execute()` / `execute_with_options()` 从 `SDKResult` 改为 typed + Response(如 `ListBuildingResponse` / `BatchGetFreebusyResponse` / `DeleteBuildingResponse`)。 + 字段对齐飞书历史版文档 Response body example(该类文档为 GuideDocumentType,无结构化 + apiSchema);无 `data` 的写操作(update/delete/instance reply)在缺省时返回 + `Default` 空响应。**v0.18 breaking**:调用方需按新 typed Response 取值,不可再按 + `resp["field"]` 索引。请求 body 仍为 `serde_json::Value`(后续可独立 typed)。 + - **删除 `openlark-user` 幻影 `SystemStatusResource::get()` / `SystemStatusGetRequest`(#377)**: 飞书 `personal_settings/v1/system_status` 仅有 6 个 API(batch_close / batch_open / create / delete / list / patch),无 `get`("获取系统状态"对应 `list`)。既有 diff --git a/crates/openlark-meeting/src/meeting_room/building/batch_get.rs b/crates/openlark-meeting/src/meeting_room/building/batch_get.rs index d8bae3b9e..e04babe22 100644 --- a/crates/openlark-meeting/src/meeting_room/building/batch_get.rs +++ b/crates/openlark-meeting/src/meeting_room/building/batch_get.rs @@ -8,6 +8,7 @@ use openlark_core::{ use crate::common::api_utils::extract_response_data; use crate::endpoints::MEETING_ROOM; +use crate::meeting_room::responses::BatchGetBuildingResponse; /// 查询建筑物详情请求 pub struct BatchGetBuildingRequest { @@ -33,14 +34,17 @@ impl BatchGetBuildingRequest { /// 执行请求 /// /// docPath: - pub async fn execute(self) -> SDKResult { + pub async fn execute(self) -> SDKResult { self.execute_with_options(RequestOption::default()).await } /// 执行请求(带选项) - pub async fn execute_with_options(self, option: RequestOption) -> SDKResult { + pub async fn execute_with_options( + self, + option: RequestOption, + ) -> SDKResult { // url: GET:/open-apis/meeting_room/building/batch_get - let mut req: ApiRequest = + let mut req: ApiRequest = ApiRequest::get(format!("{MEETING_ROOM}/building/batch_get")); for (k, v) in self.query_params { req = req.query(k, v); @@ -55,7 +59,7 @@ impl BatchGetBuildingRequest { mod tests { use super::*; - /// 端到端:GET .../meeting_room/building/batch_get → 裸 Value(单层 resp["field"])。 + /// 端到端:GET .../meeting_room/building/batch_get → BatchGetBuildingResponse。 #[tokio::test] async fn test_batch_get_building_returns_data_on_success() { use serde_json::json; @@ -71,7 +75,13 @@ mod tests { "msg": "success", "data": { "buildings": [ - { "building_id": "bldg_001", "name": "1号楼" } + { + "building_id": "bldg_001", + "name": "1号楼", + "floors": ["F1"], + "country_id": "1814991", + "district_id": "2034437" + } ] } }))) @@ -90,7 +100,8 @@ mod tests { .execute() .await .expect("查询建筑物详情应成功"); - assert_eq!(resp["buildings"][0]["building_id"], json!("bldg_001")); + assert_eq!(resp.buildings[0].building_id, "bldg_001"); + assert_eq!(resp.buildings[0].name.as_deref(), Some("1号楼")); let received = server.received_requests().await.unwrap_or_default(); assert_eq!(received.len(), 1); diff --git a/crates/openlark-meeting/src/meeting_room/building/batch_get_id.rs b/crates/openlark-meeting/src/meeting_room/building/batch_get_id.rs index 5ca1a12b0..282023c49 100644 --- a/crates/openlark-meeting/src/meeting_room/building/batch_get_id.rs +++ b/crates/openlark-meeting/src/meeting_room/building/batch_get_id.rs @@ -6,6 +6,7 @@ use openlark_core::{ SDKResult, api::ApiRequest, config::Config, http::Transport, req_option::RequestOption, }; +use crate::meeting_room::responses::BatchGetBuildingIdResponse; use crate::{common::api_utils::extract_response_data, endpoints::MEETING_ROOM}; /// 查询建筑物ID请求 @@ -32,14 +33,17 @@ impl BatchGetBuildingIdRequest { /// 执行请求 /// /// docPath: - pub async fn execute(self) -> SDKResult { + pub async fn execute(self) -> SDKResult { self.execute_with_options(RequestOption::default()).await } /// 执行请求(带选项) - pub async fn execute_with_options(self, option: RequestOption) -> SDKResult { + pub async fn execute_with_options( + self, + option: RequestOption, + ) -> SDKResult { // url: GET:/open-apis/meeting_room/building/batch_get_id - let mut req: ApiRequest = + let mut req: ApiRequest = ApiRequest::get(format!("{MEETING_ROOM}/building/batch_get_id")); for (k, v) in self.query_params { req = req.query(k, v); @@ -53,7 +57,7 @@ impl BatchGetBuildingIdRequest { mod tests { use super::*; - /// 端到端:GET .../meeting_room/building/batch_get_id → 裸 Value(单层 resp["field"])。 + /// 端到端:GET .../meeting_room/building/batch_get_id → BatchGetBuildingIdResponse。 #[tokio::test] async fn test_batch_get_id_building_returns_data_on_success() { use serde_json::json; @@ -67,7 +71,18 @@ mod tests { .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "code": 0, "msg": "success", - "data": { "building_ids": ["bldg_001", "bldg_002"] } + "data": { + "buildings": [ + { + "building_id": "bldg_001", + "custom_bulding_id": "test01" + }, + { + "building_id": "bldg_002", + "custom_bulding_id": "test02" + } + ] + } }))) .mount(&server) .await; @@ -84,7 +99,11 @@ mod tests { .execute() .await .expect("查询建筑物ID应成功"); - assert_eq!(resp["building_ids"][0], json!("bldg_001")); + assert_eq!(resp.buildings[0].building_id, "bldg_001"); + assert_eq!( + resp.buildings[0].custom_bulding_id.as_deref(), + Some("test01") + ); let received = server.received_requests().await.unwrap_or_default(); assert_eq!(received.len(), 1); diff --git a/crates/openlark-meeting/src/meeting_room/building/create.rs b/crates/openlark-meeting/src/meeting_room/building/create.rs index f8611dbd4..9f422dcd2 100644 --- a/crates/openlark-meeting/src/meeting_room/building/create.rs +++ b/crates/openlark-meeting/src/meeting_room/building/create.rs @@ -8,6 +8,7 @@ use openlark_core::{ use crate::common::api_endpoints::MeetingRoomApi; use crate::common::api_utils::{extract_response_data, serialize_params}; +use crate::meeting_room::responses::CreateBuildingResponse; /// 创建建筑物请求 pub struct CreateBuildingRequest { @@ -25,7 +26,7 @@ impl CreateBuildingRequest { /// 说明:该接口请求体字段较多,建议直接按文档构造 JSON 传入。 /// /// docPath: - pub async fn execute(self, body: serde_json::Value) -> SDKResult { + pub async fn execute(self, body: serde_json::Value) -> SDKResult { self.execute_with_options(body, RequestOption::default()) .await } @@ -35,10 +36,10 @@ impl CreateBuildingRequest { self, body: serde_json::Value, option: RequestOption, - ) -> SDKResult { + ) -> SDKResult { // url: POST:/open-apis/meeting_room/buildings let api_endpoint = MeetingRoomApi::BuildingCreate; - let req: ApiRequest = + let req: ApiRequest = ApiRequest::post(api_endpoint.to_url()).body(serialize_params(&body, "创建建筑物")?); let resp = Transport::request(req, &self.config, Some(option)).await?; @@ -74,4 +75,37 @@ mod tests { assert_eq!(request.config.app_id(), "test_app"); assert_eq!(request.config.app_secret(), "test_secret"); } + + /// 端到端:POST .../meeting_room/buildings → CreateBuildingResponse。 + #[tokio::test] + async fn test_create_building_returns_typed_on_success() { + use serde_json::json; + use wiremock::MockServer; + use wiremock::matchers::{method, path}; + use wiremock::{Mock, ResponseTemplate}; + + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/open-apis/meeting_room/buildings")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "code": 0, + "msg": "success", + "data": { "building_id": "omb_8ec170b937536a5d87c23b418b83f9bb" } + }))) + .mount(&server) + .await; + + let config = Config::builder() + .app_id("ci_app_id") + .app_secret("ci_app_secret") + .base_url(server.uri()) + .enable_token_cache(false) + .build(); + + let resp = CreateBuildingRequest::new(config) + .execute(json!({ "name": "测试建筑" })) + .await + .expect("创建建筑物应成功"); + assert_eq!(resp.building_id, "omb_8ec170b937536a5d87c23b418b83f9bb"); + } } diff --git a/crates/openlark-meeting/src/meeting_room/building/delete.rs b/crates/openlark-meeting/src/meeting_room/building/delete.rs index 657a9ca01..62f5ccd4e 100644 --- a/crates/openlark-meeting/src/meeting_room/building/delete.rs +++ b/crates/openlark-meeting/src/meeting_room/building/delete.rs @@ -8,7 +8,7 @@ use openlark_core::{ }; use crate::common::api_endpoints::MeetingRoomApi; -use crate::common::api_utils::extract_response_data; +use crate::meeting_room::responses::DeleteBuildingResponse; /// 删除建筑物请求 pub struct DeleteBuildingRequest { @@ -34,23 +34,35 @@ impl DeleteBuildingRequest { /// 执行请求 /// /// docPath: - pub async fn execute(self) -> SDKResult { + pub async fn execute(self) -> SDKResult { self.execute_with_options(RequestOption::default()).await } /// 执行请求(带选项) - pub async fn execute_with_options(self, option: RequestOption) -> SDKResult { + pub async fn execute_with_options( + self, + option: RequestOption, + ) -> SDKResult { validate_required!(self.building_id, "building_id 不能为空"); // url: DELETE:/open-apis/meeting_room/buildings/:building_id let api_endpoint = MeetingRoomApi::BuildingDelete(self.building_id.clone()); - let req: ApiRequest = - ApiRequest::delete(api_endpoint.to_url()).body(serde_json::json!({ + let req: ApiRequest = ApiRequest::delete(api_endpoint.to_url()) + .body(serde_json::json!({ "building_id": self.building_id })); let resp = Transport::request(req, &self.config, Some(option)).await?; - extract_response_data(resp, "删除建筑物") + // 官方示例无 data 字段;成功且缺省时返回空响应。 + if !resp.is_success() { + return Err(openlark_core::error::api_error( + resp.code() as u16, + "删除建筑物", + resp.message().to_string(), + resp.raw().request_id.clone(), + )); + } + Ok(resp.data.unwrap_or_default()) } } @@ -58,7 +70,7 @@ impl DeleteBuildingRequest { mod tests { use super::*; - /// 端到端:DELETE .../meeting_room/buildings/{building_id} → 裸 Value(单层 resp["field"])。 + /// 端到端:DELETE .../meeting_room/buildings/{building_id} → DeleteBuildingResponse。 #[tokio::test] async fn test_delete_building_returns_data_on_success() { use serde_json::json; @@ -71,8 +83,7 @@ mod tests { .and(path("/open-apis/meeting_room/buildings/bldg_001")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "code": 0, - "msg": "success", - "data": { "success": true } + "msg": "success" }))) .mount(&server) .await; @@ -89,7 +100,7 @@ mod tests { .execute() .await .expect("删除建筑物应成功"); - assert_eq!(resp["success"], json!(true)); + assert_eq!(resp, DeleteBuildingResponse {}); let received = server.received_requests().await.unwrap_or_default(); assert_eq!(received.len(), 1); diff --git a/crates/openlark-meeting/src/meeting_room/building/list.rs b/crates/openlark-meeting/src/meeting_room/building/list.rs index d99347727..f70f55b21 100644 --- a/crates/openlark-meeting/src/meeting_room/building/list.rs +++ b/crates/openlark-meeting/src/meeting_room/building/list.rs @@ -8,6 +8,7 @@ use openlark_core::{ use crate::common::api_utils::extract_response_data; use crate::endpoints::MEETING_ROOM; +use crate::meeting_room::responses::ListBuildingResponse; /// 获取建筑物列表请求 pub struct ListBuildingRequest { @@ -33,14 +34,17 @@ impl ListBuildingRequest { /// 执行请求 /// /// docPath: - pub async fn execute(self) -> SDKResult { + pub async fn execute(self) -> SDKResult { self.execute_with_options(RequestOption::default()).await } /// 执行请求(带选项) - pub async fn execute_with_options(self, option: RequestOption) -> SDKResult { + pub async fn execute_with_options( + self, + option: RequestOption, + ) -> SDKResult { // url: GET:/open-apis/meeting_room/building/list - let mut req: ApiRequest = + let mut req: ApiRequest = ApiRequest::get(format!("{MEETING_ROOM}/building/list")); for (k, v) in self.query_params { req = req.query(k, v); @@ -65,4 +69,54 @@ mod tests { .query_param("key1".to_string(), "value1".to_string()); let _ = request; } + + /// 端到端:GET .../meeting_room/building/list → ListBuildingResponse。 + #[tokio::test] + async fn test_list_building_returns_typed_on_success() { + use serde_json::json; + use wiremock::MockServer; + use wiremock::matchers::{method, path}; + use wiremock::{Mock, ResponseTemplate}; + + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/open-apis/meeting_room/building/list")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "code": 0, + "msg": "success", + "data": { + "page_token": "1", + "has_more": true, + "buildings": [{ + "building_id": "omb_8ec170b937536a5d87c23b418b83f9bb", + "name": "Building name", + "description": "Some description", + "floors": ["F1"], + "country_id": "country id", + "district_id": "district id" + }] + } + }))) + .mount(&server) + .await; + + let config = Config::builder() + .app_id("ci_app_id") + .app_secret("ci_app_secret") + .base_url(server.uri()) + .enable_token_cache(false) + .build(); + + let resp = ListBuildingRequest::new(config) + .query_param("page_size", "10") + .execute() + .await + .expect("获取建筑物列表应成功"); + assert_eq!(resp.has_more, Some(true)); + assert_eq!( + resp.buildings[0].building_id, + "omb_8ec170b937536a5d87c23b418b83f9bb" + ); + assert_eq!(resp.buildings[0].name.as_deref(), Some("Building name")); + } } diff --git a/crates/openlark-meeting/src/meeting_room/building/update.rs b/crates/openlark-meeting/src/meeting_room/building/update.rs index 8d9d62791..ff6a89c63 100644 --- a/crates/openlark-meeting/src/meeting_room/building/update.rs +++ b/crates/openlark-meeting/src/meeting_room/building/update.rs @@ -7,10 +7,8 @@ use openlark_core::{ validate_required, }; -use crate::{ - common::api_endpoints::MeetingRoomApi, - common::api_utils::{extract_response_data, serialize_params}, -}; +use crate::meeting_room::responses::UpdateBuildingResponse; +use crate::{common::api_endpoints::MeetingRoomApi, common::api_utils::serialize_params}; /// 更新建筑物请求 pub struct UpdateBuildingRequest { @@ -38,7 +36,7 @@ impl UpdateBuildingRequest { /// 说明:该接口请求体字段较多,建议直接按文档构造 JSON 传入。 /// /// docPath: - pub async fn execute(self, body: serde_json::Value) -> SDKResult { + pub async fn execute(self, body: serde_json::Value) -> SDKResult { self.execute_with_options(body, RequestOption::default()) .await } @@ -48,15 +46,24 @@ impl UpdateBuildingRequest { self, body: serde_json::Value, option: RequestOption, - ) -> SDKResult { + ) -> SDKResult { validate_required!(self.building_id, "building_id 不能为空"); let api_endpoint = MeetingRoomApi::BuildingPatch(self.building_id.clone()); - let req: ApiRequest = + let req: ApiRequest = ApiRequest::post(api_endpoint.to_url()).body(serialize_params(&body, "更新建筑物")?); let resp = Transport::request(req, &self.config, Some(option)).await?; - extract_response_data(resp, "更新建筑物") + // 官方示例无 data 字段;成功且缺省时返回空响应。 + if !resp.is_success() { + return Err(openlark_core::error::api_error( + resp.code() as u16, + "更新建筑物", + resp.message().to_string(), + resp.raw().request_id.clone(), + )); + } + Ok(resp.data.unwrap_or_default()) } } @@ -64,7 +71,7 @@ impl UpdateBuildingRequest { mod tests { use super::*; - /// 端到端:POST .../meeting_room/buildings/{building_id} → 裸 Value(单层 resp["field"])。 + /// 端到端:POST .../meeting_room/buildings/{building_id} → UpdateBuildingResponse。 #[tokio::test] async fn test_update_building_returns_data_on_success() { use serde_json::json; @@ -77,8 +84,7 @@ mod tests { .and(path("/open-apis/meeting_room/buildings/bldg_001")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "code": 0, - "msg": "success", - "data": { "building_id": "bldg_001" } + "msg": "success" }))) .mount(&server) .await; @@ -95,7 +101,7 @@ mod tests { .execute(json!({ "name": "更新1号楼" })) .await .expect("更新建筑物应成功"); - assert_eq!(resp["building_id"], json!("bldg_001")); + assert_eq!(resp, UpdateBuildingResponse {}); let received = server.received_requests().await.unwrap_or_default(); assert_eq!(received.len(), 1); diff --git a/crates/openlark-meeting/src/meeting_room/country/list.rs b/crates/openlark-meeting/src/meeting_room/country/list.rs index db1c0c7e9..822ad0dc4 100644 --- a/crates/openlark-meeting/src/meeting_room/country/list.rs +++ b/crates/openlark-meeting/src/meeting_room/country/list.rs @@ -6,6 +6,7 @@ use openlark_core::{ SDKResult, api::ApiRequest, config::Config, http::Transport, req_option::RequestOption, }; +use crate::meeting_room::responses::ListCountryResponse; use crate::{common::api_utils::extract_response_data, endpoints::MEETING_ROOM}; /// 获取国家地区列表请求 @@ -32,14 +33,17 @@ impl ListCountryRequest { /// 执行请求 /// /// docPath: - pub async fn execute(self) -> SDKResult { + pub async fn execute(self) -> SDKResult { self.execute_with_options(RequestOption::default()).await } /// 执行请求(带选项) - pub async fn execute_with_options(self, option: RequestOption) -> SDKResult { + pub async fn execute_with_options( + self, + option: RequestOption, + ) -> SDKResult { // url: GET:/open-apis/meeting_room/country/list - let mut req: ApiRequest = + let mut req: ApiRequest = ApiRequest::get(format!("{MEETING_ROOM}/country/list")); for (k, v) in self.query_params { req = req.query(k, v); @@ -63,4 +67,42 @@ mod tests { .query_param("key1".to_string(), "value1".to_string()); let _ = request; } + + /// 端到端:GET .../meeting_room/country/list → ListCountryResponse。 + #[tokio::test] + async fn test_list_country_returns_typed_on_success() { + use serde_json::json; + use wiremock::MockServer; + use wiremock::matchers::{method, path}; + use wiremock::{Mock, ResponseTemplate}; + + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/open-apis/meeting_room/country/list")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "code": 0, + "msg": "success", + "data": { + "countries": [ + { "country_id": "1814991", "name": "中国" } + ] + } + }))) + .mount(&server) + .await; + + let config = Config::builder() + .app_id("ci_app_id") + .app_secret("ci_app_secret") + .base_url(server.uri()) + .enable_token_cache(false) + .build(); + + let resp = ListCountryRequest::new(config) + .execute() + .await + .expect("获取国家地区列表应成功"); + assert_eq!(resp.countries[0].country_id, "1814991"); + assert_eq!(resp.countries[0].name.as_deref(), Some("中国")); + } } diff --git a/crates/openlark-meeting/src/meeting_room/district/list.rs b/crates/openlark-meeting/src/meeting_room/district/list.rs index d429efc12..228d3ed42 100644 --- a/crates/openlark-meeting/src/meeting_room/district/list.rs +++ b/crates/openlark-meeting/src/meeting_room/district/list.rs @@ -6,6 +6,7 @@ use openlark_core::{ SDKResult, api::ApiRequest, config::Config, http::Transport, req_option::RequestOption, }; +use crate::meeting_room::responses::ListDistrictResponse; use crate::{common::api_utils::extract_response_data, endpoints::MEETING_ROOM}; /// 获取城市列表请求 @@ -32,14 +33,17 @@ impl ListDistrictRequest { /// 执行请求 /// /// docPath: - pub async fn execute(self) -> SDKResult { + pub async fn execute(self) -> SDKResult { self.execute_with_options(RequestOption::default()).await } /// 执行请求(带选项) - pub async fn execute_with_options(self, option: RequestOption) -> SDKResult { + pub async fn execute_with_options( + self, + option: RequestOption, + ) -> SDKResult { // url: GET:/open-apis/meeting_room/district/list - let mut req: ApiRequest = + let mut req: ApiRequest = ApiRequest::get(format!("{MEETING_ROOM}/district/list")); for (k, v) in self.query_params { req = req.query(k, v); @@ -63,4 +67,42 @@ mod tests { .query_param("key1".to_string(), "value1".to_string()); let _ = request; } + + /// 端到端:GET .../meeting_room/district/list → ListDistrictResponse。 + #[tokio::test] + async fn test_list_district_returns_typed_on_success() { + use serde_json::json; + use wiremock::MockServer; + use wiremock::matchers::{method, path}; + use wiremock::{Mock, ResponseTemplate}; + + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/open-apis/meeting_room/district/list")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "code": 0, + "msg": "success", + "data": { + "districts": [ + { "district_id": "1796236", "name": "上海" } + ] + } + }))) + .mount(&server) + .await; + + let config = Config::builder() + .app_id("ci_app_id") + .app_secret("ci_app_secret") + .base_url(server.uri()) + .enable_token_cache(false) + .build(); + + let resp = ListDistrictRequest::new(config) + .execute() + .await + .expect("获取城市列表应成功"); + assert_eq!(resp.districts[0].district_id, "1796236"); + assert_eq!(resp.districts[0].name.as_deref(), Some("上海")); + } } diff --git a/crates/openlark-meeting/src/meeting_room/freebusy/batch_get.rs b/crates/openlark-meeting/src/meeting_room/freebusy/batch_get.rs index b7cc1b50d..812f41bbe 100644 --- a/crates/openlark-meeting/src/meeting_room/freebusy/batch_get.rs +++ b/crates/openlark-meeting/src/meeting_room/freebusy/batch_get.rs @@ -8,6 +8,7 @@ use openlark_core::{ use crate::common::api_utils::extract_response_data; use crate::endpoints::MEETING_ROOM; +use crate::meeting_room::responses::BatchGetFreebusyResponse; /// 查询会议室忙闲请求 pub struct BatchGetFreebusyRequest { @@ -33,14 +34,17 @@ impl BatchGetFreebusyRequest { /// 执行请求 /// /// docPath: - pub async fn execute(self) -> SDKResult { + pub async fn execute(self) -> SDKResult { self.execute_with_options(RequestOption::default()).await } /// 执行请求(带选项) - pub async fn execute_with_options(self, option: RequestOption) -> SDKResult { + pub async fn execute_with_options( + self, + option: RequestOption, + ) -> SDKResult { // url: GET:/open-apis/meeting_room/freebusy/batch_get - let mut req: ApiRequest = + let mut req: ApiRequest = ApiRequest::get(format!("{MEETING_ROOM}/freebusy/batch_get")); for (k, v) in self.query_params { req = req.query(k, v); @@ -55,7 +59,7 @@ impl BatchGetFreebusyRequest { mod tests { use super::*; - /// 端到端:GET .../meeting_room/freebusy/batch_get → 裸 Value(单层 resp["field"])。 + /// 端到端:GET .../meeting_room/freebusy/batch_get → BatchGetFreebusyResponse。 #[tokio::test] async fn test_batch_get_freebusy_returns_data_on_success() { use serde_json::json; @@ -70,9 +74,20 @@ mod tests { "code": 0, "msg": "success", "data": { - "freebusy_list": [ - { "room_id": "room_001", "status": "busy" } - ] + "time_max": "2019-09-04T09:45:00+08:00", + "time_min": "2019-09-04T08:45:00+08:00", + "free_busy": { + "room_001": [{ + "start_time": "2019-09-04T09:00:00+08:00", + "end_time": "2019-09-04T09:30:00+08:00", + "uid": "bff6b51f-b7c1-40c6-b8ef-aef966c9ffc7", + "original_time": 0, + "organizer_info": { + "name": "张三", + "open_id": "ou_xxx" + } + }] + } } }))) .mount(&server) @@ -90,7 +105,15 @@ mod tests { .execute() .await .expect("查询会议室忙闲应成功"); - assert_eq!(resp["freebusy_list"][0]["room_id"], json!("room_001")); + let slots = resp.free_busy.get("room_001").expect("应有 room_001 忙闲"); + assert_eq!( + slots[0].uid.as_deref(), + Some("bff6b51f-b7c1-40c6-b8ef-aef966c9ffc7") + ); + assert_eq!( + slots[0].organizer_info.as_ref().unwrap().name.as_deref(), + Some("张三") + ); let received = server.received_requests().await.unwrap_or_default(); assert_eq!(received.len(), 1); diff --git a/crates/openlark-meeting/src/meeting_room/instance/reply.rs b/crates/openlark-meeting/src/meeting_room/instance/reply.rs index 654b19ffb..ef991845b 100644 --- a/crates/openlark-meeting/src/meeting_room/instance/reply.rs +++ b/crates/openlark-meeting/src/meeting_room/instance/reply.rs @@ -7,7 +7,8 @@ use openlark_core::{ }; use crate::common::api_endpoints::MeetingRoomApi; -use crate::common::api_utils::{extract_response_data, serialize_params}; +use crate::common::api_utils::serialize_params; +use crate::meeting_room::responses::ReplyInstanceResponse; /// 回复会议室日程实例请求 pub struct ReplyInstanceRequest { @@ -25,7 +26,7 @@ impl ReplyInstanceRequest { /// 说明:该接口请求体字段较多,建议直接按文档构造 JSON 传入。 /// /// docPath: - pub async fn execute(self, body: serde_json::Value) -> SDKResult { + pub async fn execute(self, body: serde_json::Value) -> SDKResult { self.execute_with_options(body, RequestOption::default()) .await } @@ -35,13 +36,22 @@ impl ReplyInstanceRequest { self, body: serde_json::Value, option: RequestOption, - ) -> SDKResult { + ) -> SDKResult { let api_endpoint = MeetingRoomApi::InstanceReplyOld; - let req: ApiRequest = ApiRequest::post(api_endpoint.to_url()) + let req: ApiRequest = ApiRequest::post(api_endpoint.to_url()) .body(serialize_params(&body, "回复会议室日程实例")?); let resp = Transport::request(req, &self.config, Some(option)).await?; - extract_response_data(resp, "回复会议室日程实例") + // 官方示例无 data 字段;成功且缺省时返回空响应。 + if !resp.is_success() { + return Err(openlark_core::error::api_error( + resp.code() as u16, + "回复会议室日程实例", + resp.message().to_string(), + resp.raw().request_id.clone(), + )); + } + Ok(resp.data.unwrap_or_default()) } } @@ -49,7 +59,7 @@ impl ReplyInstanceRequest { mod tests { use super::*; - /// 端到端:POST .../meeting_room/instance/reply → 裸 Value(单层 resp["field"])。 + /// 端到端:POST .../meeting_room/instance/reply → ReplyInstanceResponse。 #[tokio::test] async fn test_reply_instance_returns_data_on_success() { use serde_json::json; @@ -62,8 +72,7 @@ mod tests { .and(path("/open-apis/meeting_room/instance/reply")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "code": 0, - "msg": "success", - "data": { "instance_id": "inst_001", "status": "replied" } + "msg": "success" }))) .mount(&server) .await; @@ -79,7 +88,7 @@ mod tests { .execute(json!({ "instance_id": "inst_001", "reply": "accept" })) .await .expect("回复会议室日程实例应成功"); - assert_eq!(resp["instance_id"], json!("inst_001")); + assert_eq!(resp, ReplyInstanceResponse {}); let received = server.received_requests().await.unwrap_or_default(); assert_eq!(received.len(), 1); diff --git a/crates/openlark-meeting/src/meeting_room/responses.rs b/crates/openlark-meeting/src/meeting_room/responses.rs index 85b465f24..2a8bc82ef 100644 --- a/crates/openlark-meeting/src/meeting_room/responses.rs +++ b/crates/openlark-meeting/src/meeting_room/responses.rs @@ -1,270 +1,598 @@ //! 会议室历史版本相关响应结构 //! -//! 定义会议室 API(历史版本)的响应数据类型。 +//! 字段对齐飞书历史版文档 Response body example(apiSchema 为 GuideDocumentType, +//! 无结构化 schema,以官方 JSON 示例为准)。 + +use std::collections::HashMap; use openlark_core::api::{ApiResponseTrait, ResponseFormat}; use serde::{Deserialize, Serialize}; -/// 创建会议室响应 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CreateRoomResponse { - /// 会议室 ID - pub room_id: String, +// --------------------------------------------------------------------------- +// Building +// --------------------------------------------------------------------------- + +/// 建筑物信息(list / batch_get 共用)。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct BuildingInfo { + /// 建筑物 ID。 + pub building_id: String, + /// 建筑物名称。 + #[serde(default)] + pub name: Option, + /// 描述。 + #[serde(default)] + pub description: Option, + /// 楼层列表。 + #[serde(default)] + pub floors: Option>, + /// 国家/地区 ID。 + #[serde(default)] + pub country_id: Option, + /// 城市 ID。 + #[serde(default)] + pub district_id: Option, } -impl ApiResponseTrait for CreateRoomResponse { +/// 创建建筑物响应。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct CreateBuildingResponse { + /// 建筑物 ID。 + pub building_id: String, +} + +impl ApiResponseTrait for CreateBuildingResponse { fn data_format() -> ResponseFormat { ResponseFormat::Data } } -/// 会议室响应。 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GetRoomResponse { - /// 会议室 ID。 - pub room_id: String, - /// 会议室名称。 - pub room_name: String, - /// 描述。 - pub description: Option, - /// 容量。 - pub capacity: u32, - /// 设备列表。 - pub devices: Option>, - /// 状态。 - pub status: String, +/// 获取建筑物列表响应。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ListBuildingResponse { + /// 分页标记。 + #[serde(default)] + pub page_token: Option, + /// 是否还有更多。 + #[serde(default)] + pub has_more: Option, + /// 建筑物列表。 + #[serde(default)] + pub buildings: Vec, } -impl ApiResponseTrait for GetRoomResponse { +impl ApiResponseTrait for ListBuildingResponse { fn data_format() -> ResponseFormat { ResponseFormat::Data } } -/// 会议室设备信息。 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeviceInfo { - /// 设备 ID。 - pub device_id: String, - /// 设备名称。 - pub device_name: String, - /// 设备类型。 - pub device_type: String, +/// 批量查询建筑物详情响应。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct BatchGetBuildingResponse { + /// 建筑物列表。 + #[serde(default)] + pub buildings: Vec, } -/// 批量获取会议室响应 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BatchGetRoomResponse { - /// 会议室列表 - pub rooms: Vec, +impl ApiResponseTrait for BatchGetBuildingResponse { + fn data_format() -> ResponseFormat { + ResponseFormat::Data + } } -impl ApiResponseTrait for BatchGetRoomResponse { +/// 建筑物 ID 映射(按自定义 ID 查询)。 +/// +/// 注意:官方示例字段名为 `custom_bulding_id`(拼写缺 i),保持原样对齐。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct BuildingIdMapping { + /// 建筑物 ID。 + pub building_id: String, + /// 租户自定义建筑物 ID(官方字段名拼写为 bulding)。 + #[serde(default)] + pub custom_bulding_id: Option, +} + +/// 查询建筑物 ID 响应。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct BatchGetBuildingIdResponse { + /// 建筑物 ID 映射列表。 + #[serde(default)] + pub buildings: Vec, +} + +impl ApiResponseTrait for BatchGetBuildingIdResponse { + fn data_format() -> ResponseFormat { + ResponseFormat::Data + } +} + +/// 更新建筑物响应(官方示例无 `data` 字段)。 +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +pub struct UpdateBuildingResponse {} + +impl ApiResponseTrait for UpdateBuildingResponse { + fn data_format() -> ResponseFormat { + ResponseFormat::Data + } +} + +/// 删除建筑物响应(官方示例无 `data` 字段)。 +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +pub struct DeleteBuildingResponse {} + +impl ApiResponseTrait for DeleteBuildingResponse { fn data_format() -> ResponseFormat { ResponseFormat::Data } } -/// 会议室信息 -#[derive(Debug, Clone, Serialize, Deserialize)] +// --------------------------------------------------------------------------- +// Room +// --------------------------------------------------------------------------- + +/// 会议室信息(list / batch_get 共用)。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct RoomInfo { - /// 会议室 ID + /// 会议室 ID。 pub room_id: String, - /// 会议室名称 - pub room_name: String, - /// 容量 - pub capacity: u32, - /// 描述 + /// 所属建筑物 ID。 + #[serde(default)] + pub building_id: Option, + /// 所属建筑物名称。 + #[serde(default)] + pub building_name: Option, + /// 容量。 + #[serde(default)] + pub capacity: Option, + /// 描述。 + #[serde(default)] pub description: Option, - /// 状态 - pub status: String, - /// 设备信息 - pub devices: Option>, + /// 展示 ID。 + #[serde(default)] + pub display_id: Option, + /// 楼层名称。 + #[serde(default)] + pub floor_name: Option, + /// 是否停用。 + #[serde(default)] + pub is_disabled: Option, + /// 会议室名称。 + #[serde(default)] + pub name: Option, } -/// 更新会议室响应 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UpdateRoomResponse { - /// 会议室 ID +/// 创建会议室响应。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct CreateRoomResponse { + /// 会议室 ID。 pub room_id: String, } -impl ApiResponseTrait for UpdateRoomResponse { +impl ApiResponseTrait for CreateRoomResponse { fn data_format() -> ResponseFormat { ResponseFormat::Data } } -/// 删除会议室响应 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeleteRoomResponse {} +/// 获取会议室列表响应。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ListRoomResponse { + /// 分页标记。 + #[serde(default)] + pub page_token: Option, + /// 是否还有更多。 + #[serde(default)] + pub has_more: Option, + /// 会议室列表。 + #[serde(default)] + pub rooms: Vec, +} -impl ApiResponseTrait for DeleteRoomResponse { +impl ApiResponseTrait for ListRoomResponse { fn data_format() -> ResponseFormat { ResponseFormat::Data } } -/// 创建建筑响应 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CreateBuildingResponse { - /// 建筑 ID - pub building_id: String, +/// 批量查询会议室详情响应。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct BatchGetRoomResponse { + /// 会议室列表。 + #[serde(default)] + pub rooms: Vec, } -impl ApiResponseTrait for CreateBuildingResponse { +impl ApiResponseTrait for BatchGetRoomResponse { fn data_format() -> ResponseFormat { ResponseFormat::Data } } -/// 获取建筑响应 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GetBuildingResponse { - /// 建筑 ID - pub building_id: String, - /// 建筑名称 - pub name: String, - /// 地址 - pub address: Option, - /// 城市 - pub city: Option, +/// 会议室 ID 映射(按自定义 ID 查询)。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct RoomIdMapping { + /// 会议室 ID。 + pub room_id: String, + /// 租户自定义会议室 ID。 + #[serde(default)] + pub custom_room_id: Option, +} + +/// 查询会议室 ID 响应。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct BatchGetRoomIdResponse { + /// 会议室 ID 映射列表。 + #[serde(default)] + pub rooms: Vec, } -impl ApiResponseTrait for GetBuildingResponse { +impl ApiResponseTrait for BatchGetRoomIdResponse { fn data_format() -> ResponseFormat { ResponseFormat::Data } } -/// 建筑信息 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BuildingInfo { - /// 建筑 ID - pub building_id: String, - /// 建筑名称 - pub name: String, - /// 地址 - pub address: Option, - /// 城市 - pub city: Option, -} - -/// 批量获取建筑响应 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BatchGetBuildingResponse { - /// 建筑列表 - pub buildings: Vec, -} +/// 更新会议室响应(官方示例无 `data` 字段)。 +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +pub struct UpdateRoomResponse {} -impl ApiResponseTrait for BatchGetBuildingResponse { +impl ApiResponseTrait for UpdateRoomResponse { fn data_format() -> ResponseFormat { ResponseFormat::Data } } -/// 更新建筑响应 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UpdateBuildingResponse { - /// 建筑 ID - pub building_id: String, -} +/// 删除会议室响应(官方示例无 `data` 字段)。 +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +pub struct DeleteRoomResponse {} -impl ApiResponseTrait for UpdateBuildingResponse { +impl ApiResponseTrait for DeleteRoomResponse { fn data_format() -> ResponseFormat { ResponseFormat::Data } } -/// 删除建筑响应 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeleteBuildingResponse {} +// --------------------------------------------------------------------------- +// Country / District +// --------------------------------------------------------------------------- + +/// 国家/地区信息。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct CountryInfo { + /// 国家/地区 ID。 + pub country_id: String, + /// 名称。 + #[serde(default)] + pub name: Option, +} -impl ApiResponseTrait for DeleteBuildingResponse { +/// 获取国家/地区列表响应。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ListCountryResponse { + /// 国家/地区列表。 + #[serde(default)] + pub countries: Vec, +} + +impl ApiResponseTrait for ListCountryResponse { fn data_format() -> ResponseFormat { ResponseFormat::Data } } -/// 批量获取建筑响应(按 ID) -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BatchGetBuildingByIdResponse { - /// 建筑列表 - pub buildings: Vec, +/// 城市信息。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct DistrictInfo { + /// 城市 ID。 + pub district_id: String, + /// 名称。 + #[serde(default)] + pub name: Option, +} + +/// 获取城市列表响应。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ListDistrictResponse { + /// 城市列表。 + #[serde(default)] + pub districts: Vec, } -impl ApiResponseTrait for BatchGetBuildingByIdResponse { +impl ApiResponseTrait for ListDistrictResponse { fn data_format() -> ResponseFormat { ResponseFormat::Data } } -/// 批量获取会议实例响应 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BatchGetInstanceResponse { - /// 实例列表 - pub instances: Vec, +// --------------------------------------------------------------------------- +// Freebusy +// --------------------------------------------------------------------------- + +/// 日程组织者信息。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct FreeBusyOrganizer { + /// 组织者姓名。 + #[serde(default)] + pub name: Option, + /// 组织者 open_id。 + #[serde(default)] + pub open_id: Option, +} + +/// 会议室忙闲时段。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct FreeBusySlot { + /// 开始时间(RFC3339)。 + #[serde(default)] + pub start_time: Option, + /// 结束时间(RFC3339)。 + #[serde(default)] + pub end_time: Option, + /// 日程 UID。 + #[serde(default)] + pub uid: Option, + /// 重复日程原始时间(Unix 秒;非重复为 0)。 + #[serde(default)] + pub original_time: Option, + /// 组织者信息。 + #[serde(default)] + pub organizer_info: Option, +} + +/// 查询会议室忙闲响应。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct BatchGetFreebusyResponse { + /// 查询时间上限。 + #[serde(default)] + pub time_max: Option, + /// 查询时间下限。 + #[serde(default)] + pub time_min: Option, + /// 按会议室 ID 索引的忙闲时段列表。 + #[serde(default)] + pub free_busy: HashMap>, } -impl ApiResponseTrait for BatchGetInstanceResponse { +impl ApiResponseTrait for BatchGetFreebusyResponse { fn data_format() -> ResponseFormat { ResponseFormat::Data } } -/// 会议实例信息 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MeetingInstanceInfo { - /// 实例 ID - pub instance_id: String, - /// 会议室 ID - pub room_id: String, - /// 会议主题 - pub topic: String, - /// 开始时间 - pub start_time: String, - /// 结束时间 - pub end_time: String, - /// 状态 - pub status: String, - /// 创建人 - pub creator: UserInfo, -} - -/// 用户信息 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UserInfo { - /// 用户 ID - pub user_id: String, - /// 用户名称 - pub name: String, - /// 用户类型 - pub user_type: String, -} - -/// 会议汇总响应 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MeetingSummaryResponse { - /// 汇总数据 - pub data: SummaryData, -} - -impl ApiResponseTrait for MeetingSummaryResponse { +// --------------------------------------------------------------------------- +// Instance reply +// --------------------------------------------------------------------------- + +/// 回复会议室日程实例响应(官方示例无 `data` 字段)。 +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +pub struct ReplyInstanceResponse {} + +impl ApiResponseTrait for ReplyInstanceResponse { fn data_format() -> ResponseFormat { ResponseFormat::Data } } -/// 汇总数据 -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SummaryData { - /// 会议总数 - pub total_meetings: u32, - /// 预约总数 - pub total_reservations: u32, - /// 活跃会议室数 - pub active_rooms: u32, +// --------------------------------------------------------------------------- +// Summary +// --------------------------------------------------------------------------- + +/// 视频会议信息(日程主题详情)。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct SummaryVchat { + /// 视频会议类型(`vc` / `third_party` / `no_meeting` 等)。 + #[serde(default)] + pub vc_type: Option, + /// 第三方视频会议 icon 类型。 + #[serde(default)] + pub icon_type: Option, + /// 第三方视频会议文案。 + #[serde(default)] + pub description: Option, + /// 视频会议 URL。 + #[serde(default)] + pub meeting_url: Option, +} + +/// 会议室日程主题与会议详情。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct SummaryEventInfo { + /// 重复日程原始时间。 + #[serde(default)] + pub original_time: Option, + /// 日程主题。 + #[serde(default)] + pub summary: Option, + /// 日程 UID。 + #[serde(default)] + pub uid: Option, + /// 视频会议信息。 + #[serde(default)] + pub vchat: Option, +} + +/// 查询失败的日程信息(`ErrorEventUids` 元素)。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct SummaryErrorEvent { + /// 日程 UID。 + #[serde(default)] + pub uid: Option, + /// 重复日程原始时间。 + #[serde(default)] + pub original_time: Option, + /// 错误信息。 + #[serde(default)] + pub error_msg: Option, +} + +/// 查询会议室日程主题和会议详情响应。 +/// +/// 官方文档使用 PascalCase 字段名(`ErrorEventUids` / `EventInfos`)。 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct BatchGetSummaryResponse { + /// 没有查询到的日程信息。 + #[serde(rename = "ErrorEventUids", default)] + pub error_event_uids: Vec, + /// 事件详情列表。 + #[serde(rename = "EventInfos", default)] + pub event_infos: Vec, +} + +impl ApiResponseTrait for BatchGetSummaryResponse { + fn data_format() -> ResponseFormat { + ResponseFormat::Data + } } #[cfg(test)] -mod tests {} +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn deserialize_list_building_from_official_example() { + let v = json!({ + "page_token": "1", + "has_more": true, + "buildings": [{ + "building_id": "omb_8ec170b937536a5d87c23b418b83f9bb", + "description": "Some description", + "floors": ["F1"], + "name": "Building name", + "country_id": "country id", + "district_id": "district id" + }] + }); + let resp: ListBuildingResponse = serde_json::from_value(v).unwrap(); + assert!(resp.has_more.unwrap()); + assert_eq!(resp.buildings[0].name.as_deref(), Some("Building name")); + assert_eq!( + resp.buildings[0].floors.as_ref().unwrap(), + &vec!["F1".to_string()] + ); + } + + #[test] + fn deserialize_batch_get_building_id_preserves_official_typo() { + let v = json!({ + "buildings": [{ + "building_id": "omb_xxx", + "custom_bulding_id": "test01" + }] + }); + let resp: BatchGetBuildingIdResponse = serde_json::from_value(v).unwrap(); + assert_eq!( + resp.buildings[0].custom_bulding_id.as_deref(), + Some("test01") + ); + } + + #[test] + fn deserialize_room_list_from_official_example() { + let v = json!({ + "page_token": "1", + "has_more": true, + "rooms": [{ + "room_id": "omm_eada1d61a550955240c28757e7dec3af", + "building_id": "omb_8ec170b937536a5d87c23b418b83f9bb", + "building_name": "Building name", + "capacity": 14, + "description": "Some description", + "display_id": "FM537532166", + "floor_name": "F1", + "is_disabled": false, + "name": "Room name" + }] + }); + let resp: ListRoomResponse = serde_json::from_value(v).unwrap(); + assert_eq!(resp.rooms[0].capacity, Some(14)); + assert_eq!(resp.rooms[0].is_disabled, Some(false)); + } + + #[test] + fn deserialize_freebusy_map_keys_are_room_ids() { + let v = json!({ + "time_max": "2019-09-04T09:45:00+08:00", + "time_min": "2019-09-04T08:45:00+08:00", + "free_busy": { + "omm_83d09ad4f6896e02029a6a075f71c9d1": [{ + "end_time": "2019-09-04T09:30:00+08:00", + "start_time": "2019-09-04T09:00:00+08:00", + "original_time": 0, + "uid": "bff6b51f-b7c1-40c6-b8ef-aef966c9ffc7", + "organizer_info": { + "name": "张三", + "open_id": "ou_xxx" + } + }] + } + }); + let resp: BatchGetFreebusyResponse = serde_json::from_value(v).unwrap(); + let slots = resp + .free_busy + .get("omm_83d09ad4f6896e02029a6a075f71c9d1") + .unwrap(); + assert_eq!(slots[0].original_time, Some(0)); + assert_eq!( + slots[0].organizer_info.as_ref().unwrap().name.as_deref(), + Some("张三") + ); + } + + #[test] + fn deserialize_summary_pascal_case_fields() { + let v = json!({ + "ErrorEventUids": [{ + "uid": "missing-uid", + "original_time": 0, + "error_msg": "not found" + }], + "EventInfos": [{ + "original_time": 0, + "summary": "test", + "uid": "a04dbea1-86b9-4372-aa8d-64ebe801be2a", + "vchat": { + "meeting_url": "https://vc.feishu.cn/j/935314044", + "vc_type": "third_party", + "icon_type": "default", + "description": "外部会议" + } + }] + }); + let resp: BatchGetSummaryResponse = serde_json::from_value(v).unwrap(); + assert_eq!(resp.error_event_uids.len(), 1); + assert_eq!( + resp.error_event_uids[0].error_msg.as_deref(), + Some("not found") + ); + assert_eq!(resp.event_infos[0].summary.as_deref(), Some("test")); + let vchat = resp.event_infos[0].vchat.as_ref().unwrap(); + assert_eq!(vchat.vc_type.as_deref(), Some("third_party")); + assert_eq!(vchat.icon_type.as_deref(), Some("default")); + assert_eq!(vchat.description.as_deref(), Some("外部会议")); + } + + #[test] + fn deserialize_empty_delete_response() { + let resp: DeleteBuildingResponse = serde_json::from_value(json!({})).unwrap(); + let _ = resp; + let resp2 = DeleteBuildingResponse::default(); + assert_eq!(resp2, DeleteBuildingResponse {}); + } + + #[test] + fn deserialize_country_and_district_list() { + let countries: ListCountryResponse = serde_json::from_value(json!({ + "countries": [{"country_id": "1814991", "name": "中国"}] + })) + .unwrap(); + assert_eq!(countries.countries[0].name.as_deref(), Some("中国")); + + let districts: ListDistrictResponse = serde_json::from_value(json!({ + "districts": [{"district_id": "1796236", "name": "上海"}] + })) + .unwrap(); + assert_eq!(districts.districts[0].district_id, "1796236"); + } +} diff --git a/crates/openlark-meeting/src/meeting_room/room/batch_get.rs b/crates/openlark-meeting/src/meeting_room/room/batch_get.rs index 13572724a..7e1e1627c 100644 --- a/crates/openlark-meeting/src/meeting_room/room/batch_get.rs +++ b/crates/openlark-meeting/src/meeting_room/room/batch_get.rs @@ -8,6 +8,7 @@ use openlark_core::{ use crate::common::api_utils::extract_response_data; use crate::endpoints::MEETING_ROOM; +use crate::meeting_room::responses::BatchGetRoomResponse; /// 查询会议室详情请求 pub struct BatchGetRoomRequest { @@ -33,14 +34,17 @@ impl BatchGetRoomRequest { /// 执行请求 /// /// docPath: - pub async fn execute(self) -> SDKResult { + pub async fn execute(self) -> SDKResult { self.execute_with_options(RequestOption::default()).await } /// 执行请求(带选项) - pub async fn execute_with_options(self, option: RequestOption) -> SDKResult { + pub async fn execute_with_options( + self, + option: RequestOption, + ) -> SDKResult { // url: GET:/open-apis/meeting_room/room/batch_get - let mut req: ApiRequest = + let mut req: ApiRequest = ApiRequest::get(format!("{MEETING_ROOM}/room/batch_get")); for (k, v) in self.query_params { req = req.query(k, v); @@ -54,7 +58,7 @@ impl BatchGetRoomRequest { mod tests { use super::*; - /// 端到端:GET .../meeting_room/room/batch_get → 裸 Value(单层 resp["field"])。 + /// 端到端:GET .../meeting_room/room/batch_get → BatchGetRoomResponse。 #[tokio::test] async fn test_batch_get_room_returns_data_on_success() { use serde_json::json; @@ -70,7 +74,12 @@ mod tests { "msg": "success", "data": { "rooms": [ - { "room_id": "room_001", "name": "大会议室" } + { + "room_id": "room_001", + "name": "大会议室", + "capacity": 20, + "is_disabled": false + } ] } }))) @@ -89,7 +98,8 @@ mod tests { .execute() .await .expect("查询会议室详情应成功"); - assert_eq!(resp["rooms"][0]["room_id"], json!("room_001")); + assert_eq!(resp.rooms[0].room_id, "room_001"); + assert_eq!(resp.rooms[0].name.as_deref(), Some("大会议室")); let received = server.received_requests().await.unwrap_or_default(); assert_eq!(received.len(), 1); diff --git a/crates/openlark-meeting/src/meeting_room/room/batch_get_id.rs b/crates/openlark-meeting/src/meeting_room/room/batch_get_id.rs index d96d61766..ed58eecba 100644 --- a/crates/openlark-meeting/src/meeting_room/room/batch_get_id.rs +++ b/crates/openlark-meeting/src/meeting_room/room/batch_get_id.rs @@ -6,6 +6,7 @@ use openlark_core::{ SDKResult, api::ApiRequest, config::Config, http::Transport, req_option::RequestOption, }; +use crate::meeting_room::responses::BatchGetRoomIdResponse; use crate::{common::api_utils::extract_response_data, endpoints::MEETING_ROOM}; /// 查询会议室ID请求 @@ -32,14 +33,17 @@ impl BatchGetRoomIdRequest { /// 执行请求 /// /// docPath: - pub async fn execute(self) -> SDKResult { + pub async fn execute(self) -> SDKResult { self.execute_with_options(RequestOption::default()).await } /// 执行请求(带选项) - pub async fn execute_with_options(self, option: RequestOption) -> SDKResult { + pub async fn execute_with_options( + self, + option: RequestOption, + ) -> SDKResult { // url: GET:/open-apis/meeting_room/room/batch_get_id - let mut req: ApiRequest = + let mut req: ApiRequest = ApiRequest::get(format!("{MEETING_ROOM}/room/batch_get_id")); for (k, v) in self.query_params { req = req.query(k, v); @@ -53,7 +57,7 @@ impl BatchGetRoomIdRequest { mod tests { use super::*; - /// 端到端:GET .../meeting_room/room/batch_get_id → 裸 Value(单层 resp["field"])。 + /// 端到端:GET .../meeting_room/room/batch_get_id → BatchGetRoomIdResponse。 #[tokio::test] async fn test_batch_get_id_room_returns_data_on_success() { use serde_json::json; @@ -67,7 +71,12 @@ mod tests { .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "code": 0, "msg": "success", - "data": { "room_ids": ["room_001", "room_002"] } + "data": { + "rooms": [ + { "room_id": "room_001", "custom_room_id": "test01" }, + { "room_id": "room_002", "custom_room_id": "test02" } + ] + } }))) .mount(&server) .await; @@ -84,7 +93,8 @@ mod tests { .execute() .await .expect("查询会议室ID应成功"); - assert_eq!(resp["room_ids"][0], json!("room_001")); + assert_eq!(resp.rooms[0].room_id, "room_001"); + assert_eq!(resp.rooms[0].custom_room_id.as_deref(), Some("test01")); let received = server.received_requests().await.unwrap_or_default(); assert_eq!(received.len(), 1); diff --git a/crates/openlark-meeting/src/meeting_room/room/create.rs b/crates/openlark-meeting/src/meeting_room/room/create.rs index ff0fc412d..6fb9353a3 100644 --- a/crates/openlark-meeting/src/meeting_room/room/create.rs +++ b/crates/openlark-meeting/src/meeting_room/room/create.rs @@ -8,6 +8,7 @@ use openlark_core::{ use crate::common::api_endpoints::MeetingRoomApi; use crate::common::api_utils::{extract_response_data, serialize_params}; +use crate::meeting_room::responses::CreateRoomResponse; /// 创建会议室请求 pub struct CreateRoomRequest { @@ -25,7 +26,7 @@ impl CreateRoomRequest { /// 说明:该接口请求体字段较多,建议直接按文档构造 JSON 传入。 /// /// docPath: - pub async fn execute(self, body: serde_json::Value) -> SDKResult { + pub async fn execute(self, body: serde_json::Value) -> SDKResult { self.execute_with_options(body, RequestOption::default()) .await } @@ -35,10 +36,10 @@ impl CreateRoomRequest { self, body: serde_json::Value, option: RequestOption, - ) -> SDKResult { + ) -> SDKResult { // url: POST:/open-apis/meeting_room/rooms let api_endpoint = MeetingRoomApi::RoomCreate; - let req: ApiRequest = + let req: ApiRequest = ApiRequest::post(api_endpoint.to_url()).body(serialize_params(&body, "创建会议室")?); let resp = Transport::request(req, &self.config, Some(option)).await?; @@ -74,4 +75,37 @@ mod tests { assert_eq!(request.config.app_id(), "test_app"); assert_eq!(request.config.app_secret(), "test_secret"); } + + /// 端到端:POST .../meeting_room/rooms → CreateRoomResponse。 + #[tokio::test] + async fn test_create_room_returns_typed_on_success() { + use serde_json::json; + use wiremock::MockServer; + use wiremock::matchers::{method, path}; + use wiremock::{Mock, ResponseTemplate}; + + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/open-apis/meeting_room/rooms")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "code": 0, + "msg": "success", + "data": { "room_id": "omm_eada1d61a550955240c28757e7dec3af" } + }))) + .mount(&server) + .await; + + let config = Config::builder() + .app_id("ci_app_id") + .app_secret("ci_app_secret") + .base_url(server.uri()) + .enable_token_cache(false) + .build(); + + let resp = CreateRoomRequest::new(config) + .execute(json!({ "name": "测试会议室", "capacity": 10 })) + .await + .expect("创建会议室应成功"); + assert_eq!(resp.room_id, "omm_eada1d61a550955240c28757e7dec3af"); + } } diff --git a/crates/openlark-meeting/src/meeting_room/room/delete.rs b/crates/openlark-meeting/src/meeting_room/room/delete.rs index 6c6d53e23..72dd6b047 100644 --- a/crates/openlark-meeting/src/meeting_room/room/delete.rs +++ b/crates/openlark-meeting/src/meeting_room/room/delete.rs @@ -8,7 +8,7 @@ use openlark_core::{ }; use crate::common::api_endpoints::MeetingRoomApi; -use crate::common::api_utils::extract_response_data; +use crate::meeting_room::responses::DeleteRoomResponse; /// 删除会议室请求 pub struct DeleteRoomRequest { @@ -34,23 +34,35 @@ impl DeleteRoomRequest { /// 执行请求 /// /// docPath: - pub async fn execute(self) -> SDKResult { + pub async fn execute(self) -> SDKResult { self.execute_with_options(RequestOption::default()).await } /// 执行请求(带选项) - pub async fn execute_with_options(self, option: RequestOption) -> SDKResult { + pub async fn execute_with_options( + self, + option: RequestOption, + ) -> SDKResult { validate_required!(self.room_id, "room_id 不能为空"); // url: DELETE:/open-apis/meeting_room/rooms/:room_id let api_endpoint = MeetingRoomApi::RoomDelete(self.room_id.clone()); - let req: ApiRequest = + let req: ApiRequest = ApiRequest::delete(api_endpoint.to_url()).body(serde_json::json!({ "room_id": self.room_id })); let resp = Transport::request(req, &self.config, Some(option)).await?; - extract_response_data(resp, "删除会议室") + // 官方示例无 data 字段;成功且缺省时返回空响应。 + if !resp.is_success() { + return Err(openlark_core::error::api_error( + resp.code() as u16, + "删除会议室", + resp.message().to_string(), + resp.raw().request_id.clone(), + )); + } + Ok(resp.data.unwrap_or_default()) } } @@ -58,7 +70,7 @@ impl DeleteRoomRequest { mod tests { use super::*; - /// 端到端:DELETE .../meeting_room/rooms/{room_id} → 裸 Value(单层 resp["field"])。 + /// 端到端:DELETE .../meeting_room/rooms/{room_id} → DeleteRoomResponse。 #[tokio::test] async fn test_delete_room_returns_data_on_success() { use serde_json::json; @@ -71,8 +83,7 @@ mod tests { .and(path("/open-apis/meeting_room/rooms/room_001")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "code": 0, - "msg": "success", - "data": { "success": true } + "msg": "success" }))) .mount(&server) .await; @@ -89,7 +100,7 @@ mod tests { .execute() .await .expect("删除会议室应成功"); - assert_eq!(resp["success"], json!(true)); + assert_eq!(resp, DeleteRoomResponse {}); let received = server.received_requests().await.unwrap_or_default(); assert_eq!(received.len(), 1); diff --git a/crates/openlark-meeting/src/meeting_room/room/list.rs b/crates/openlark-meeting/src/meeting_room/room/list.rs index 9339dcd5a..a14936ce6 100644 --- a/crates/openlark-meeting/src/meeting_room/room/list.rs +++ b/crates/openlark-meeting/src/meeting_room/room/list.rs @@ -8,6 +8,7 @@ use openlark_core::{ use crate::common::api_utils::extract_response_data; use crate::endpoints::MEETING_ROOM; +use crate::meeting_room::responses::ListRoomResponse; /// 获取会议室列表请求 pub struct ListRoomRequest { @@ -33,14 +34,14 @@ impl ListRoomRequest { /// 执行请求 /// /// docPath: - pub async fn execute(self) -> SDKResult { + pub async fn execute(self) -> SDKResult { self.execute_with_options(RequestOption::default()).await } /// 执行请求(带选项) - pub async fn execute_with_options(self, option: RequestOption) -> SDKResult { + pub async fn execute_with_options(self, option: RequestOption) -> SDKResult { // url: GET:/open-apis/meeting_room/room/list - let mut req: ApiRequest = + let mut req: ApiRequest = ApiRequest::get(format!("{MEETING_ROOM}/room/list")); for (k, v) in self.query_params { req = req.query(k, v); @@ -91,4 +92,54 @@ mod tests { ("page_size".to_string(), "50".to_string()) ); } + + /// 端到端:GET .../meeting_room/room/list → ListRoomResponse。 + #[tokio::test] + async fn test_list_room_returns_typed_on_success() { + use serde_json::json; + use wiremock::MockServer; + use wiremock::matchers::{method, path}; + use wiremock::{Mock, ResponseTemplate}; + + let server = MockServer::start().await; + Mock::given(method("GET")) + .and(path("/open-apis/meeting_room/room/list")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "code": 0, + "msg": "success", + "data": { + "page_token": "1", + "has_more": false, + "rooms": [{ + "room_id": "omm_eada1d61a550955240c28757e7dec3af", + "building_id": "omb_8ec170b937536a5d87c23b418b83f9bb", + "building_name": "Building name", + "capacity": 14, + "description": "Some description", + "display_id": "FM537532166", + "floor_name": "F1", + "is_disabled": false, + "name": "Room name" + }] + } + }))) + .mount(&server) + .await; + + let config = Config::builder() + .app_id("ci_app_id") + .app_secret("ci_app_secret") + .base_url(server.uri()) + .enable_token_cache(false) + .build(); + + let resp = ListRoomRequest::new(config) + .query_param("building_id", "bld_123") + .execute() + .await + .expect("获取会议室列表应成功"); + assert_eq!(resp.has_more, Some(false)); + assert_eq!(resp.rooms[0].capacity, Some(14)); + assert_eq!(resp.rooms[0].name.as_deref(), Some("Room name")); + } } diff --git a/crates/openlark-meeting/src/meeting_room/room/update.rs b/crates/openlark-meeting/src/meeting_room/room/update.rs index e58394d8d..9d16b74f2 100644 --- a/crates/openlark-meeting/src/meeting_room/room/update.rs +++ b/crates/openlark-meeting/src/meeting_room/room/update.rs @@ -7,10 +7,8 @@ use openlark_core::{ validate_required, }; -use crate::{ - common::api_endpoints::MeetingRoomApi, - common::api_utils::{extract_response_data, serialize_params}, -}; +use crate::meeting_room::responses::UpdateRoomResponse; +use crate::{common::api_endpoints::MeetingRoomApi, common::api_utils::serialize_params}; /// 更新会议室请求 pub struct UpdateRoomRequest { @@ -38,7 +36,7 @@ impl UpdateRoomRequest { /// 说明:该接口请求体字段较多,建议直接按文档构造 JSON 传入。 /// /// docPath: - pub async fn execute(self, body: serde_json::Value) -> SDKResult { + pub async fn execute(self, body: serde_json::Value) -> SDKResult { self.execute_with_options(body, RequestOption::default()) .await } @@ -48,15 +46,24 @@ impl UpdateRoomRequest { self, body: serde_json::Value, option: RequestOption, - ) -> SDKResult { + ) -> SDKResult { validate_required!(self.room_id, "room_id 不能为空"); let api_endpoint = MeetingRoomApi::RoomUpdate; - let req: ApiRequest = + let req: ApiRequest = ApiRequest::post(api_endpoint.to_url()).body(serialize_params(&body, "更新会议室")?); let resp = Transport::request(req, &self.config, Some(option)).await?; - extract_response_data(resp, "更新会议室") + // 官方示例无 data 字段;成功且缺省时返回空响应。 + if !resp.is_success() { + return Err(openlark_core::error::api_error( + resp.code() as u16, + "更新会议室", + resp.message().to_string(), + resp.raw().request_id.clone(), + )); + } + Ok(resp.data.unwrap_or_default()) } } @@ -64,7 +71,7 @@ impl UpdateRoomRequest { mod tests { use super::*; - /// 端到端:POST .../meeting_room/room/update → 裸 Value(单层 resp["field"])。 + /// 端到端:POST .../meeting_room/room/update → UpdateRoomResponse。 #[tokio::test] async fn test_update_room_returns_data_on_success() { use serde_json::json; @@ -77,8 +84,7 @@ mod tests { .and(path("/open-apis/meeting_room/room/update")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "code": 0, - "msg": "success", - "data": { "room_id": "room_001" } + "msg": "success" }))) .mount(&server) .await; @@ -95,7 +101,7 @@ mod tests { .execute(json!({ "name": "更新大会议室" })) .await .expect("更新会议室应成功"); - assert_eq!(resp["room_id"], json!("room_001")); + assert_eq!(resp, UpdateRoomResponse {}); let received = server.received_requests().await.unwrap_or_default(); assert_eq!(received.len(), 1); diff --git a/crates/openlark-meeting/src/meeting_room/summary/batch_get.rs b/crates/openlark-meeting/src/meeting_room/summary/batch_get.rs index c0b0da115..0b5e5ae2e 100644 --- a/crates/openlark-meeting/src/meeting_room/summary/batch_get.rs +++ b/crates/openlark-meeting/src/meeting_room/summary/batch_get.rs @@ -8,6 +8,7 @@ use openlark_core::{ use crate::common::api_endpoints::MeetingRoomApi; use crate::common::api_utils::{extract_response_data, serialize_params}; +use crate::meeting_room::responses::BatchGetSummaryResponse; /// 查询会议室日程主题和会议详情请求 pub struct BatchGetSummaryRequest { @@ -25,7 +26,7 @@ impl BatchGetSummaryRequest { /// 说明:该接口请求体字段较多,建议直接按文档构造 JSON 传入。 /// /// docPath: - pub async fn execute(self, body: serde_json::Value) -> SDKResult { + pub async fn execute(self, body: serde_json::Value) -> SDKResult { self.execute_with_options(body, RequestOption::default()) .await } @@ -35,9 +36,9 @@ impl BatchGetSummaryRequest { self, body: serde_json::Value, option: RequestOption, - ) -> SDKResult { + ) -> SDKResult { let api_endpoint = MeetingRoomApi::RoomBatchGetSummary; - let req: ApiRequest = ApiRequest::post(api_endpoint.to_url()) + let req: ApiRequest = ApiRequest::post(api_endpoint.to_url()) .body(serialize_params(&body, "查询会议室日程主题和会议详情")?); let resp = Transport::request(req, &self.config, Some(option)).await?; @@ -49,7 +50,7 @@ impl BatchGetSummaryRequest { mod tests { use super::*; - /// 端到端:POST .../meeting_room/rooms/batch_get_summary → 裸 Value(单层 resp["field"])。 + /// 端到端:POST .../meeting_room/rooms/batch_get_summary → BatchGetSummaryResponse。 #[tokio::test] async fn test_batch_get_summary_returns_data_on_success() { use serde_json::json; @@ -64,9 +65,16 @@ mod tests { "code": 0, "msg": "success", "data": { - "summaries": [ - { "room_id": "room_001", "topic": "周会" } - ] + "ErrorEventUids": [], + "EventInfos": [{ + "original_time": 0, + "summary": "周会", + "uid": "a04dbea1-86b9-4372-aa8d-64ebe801be2a", + "vchat": { + "meeting_url": "https://vc.feishu.cn/j/935314044", + "vc_type": "vc" + } + }] } }))) .mount(&server) @@ -83,7 +91,8 @@ mod tests { .execute(json!({ "room_ids": ["room_001"] })) .await .expect("查询会议室日程主题和会议详情应成功"); - assert_eq!(resp["summaries"][0]["room_id"], json!("room_001")); + assert!(resp.error_event_uids.is_empty()); + assert_eq!(resp.event_infos[0].summary.as_deref(), Some("周会")); let received = server.received_requests().await.unwrap_or_default(); assert_eq!(received.len(), 1);