Skip to main content

openlark_docs/common/
request_builder.rs

1//! 强制 Builder 模式实现
2//!
3//! 确保请求对象在构建时必填字段已设置,避免运行时验证错误。
4//!
5//! # 设计理念
6//!
7//! - **编译时验证**: 在 build() 时验证所有必填字段
8//! - **类型安全**: 使用 `Option<T>` 明确标记字段状态
9//! - **零开销**: 仅在构建时验证,运行时无额外开销
10//! - **向后兼容**: 保留旧 API,标记为 deprecated
11//!
12//! # 使用示例
13//!
14//! ```rust,ignore
15//! use openlark_docs::common::request_builder::impl_required_builder;
16//!
17//! impl_required_builder!(
18//!     CreateRecordRequest,
19//!     CreateRecordBuilder,
20//!     required: [
21//!         app_token: String,
22//!         table_id: String
23//!     ],
24//!     optional: [
25//!         user_id_type: String,
26//!         client_token: String
27//!     ]
28//! );
29//!
30//! // 使用方式
31//! let request = CreateRecordRequest::builder()
32//!     .app_token("app_xxx")
33//!     .table_id("table_xxx")
34//!     .user_id_type("open_id")
35//!     .config(config)
36//!     .build()?;
37//! ```
38
39/// 强制构建器宏
40///
41/// 为 Request 结构生成类型安全的 Builder,确保必填字段在编译时被验证。
42///
43/// # 参数说明
44///
45/// - `$request_name`: 请求结构体名称
46/// - `$builder_name`: 构建器结构体名称
47/// - `required: [...]`: 必填字段列表(格式: `field_name: FieldType`)
48/// - `optional: [...]`: 可选字段列表(格式: `field_name: FieldType`)
49///
50/// # 生成内容
51///
52/// 该宏会生成:
53/// 1. Builder 结构体(所有字段都是 `Option<T>`)
54/// 2. Builder::default() 方法(通过 `#[derive(Default)]`)
55/// 3. 每个字段的 setter 方法
56/// 4. Builder::build() 方法(验证必填字段)
57/// 5. Request::builder() 关联方法
58///
59/// # 示例
60///
61/// ```rust,ignore
62/// impl_required_builder!(
63///     CreateRecordRequest,
64///     CreateRecordBuilder,
65///     required: [
66///         app_token: String,
67///         table_id: String
68///     ],
69///     optional: [
70///         user_id_type: String,
71///         client_token: String
72///     ]
73/// );
74/// ```
75#[macro_export]
76macro_rules! impl_required_builder {
77    (
78        $request_name:ident,
79        $builder_name:ident,
80        required: [$($req_field:ident: $req_type:ty),* $(,)?],
81        optional: [$($opt_field:ident: $opt_type:ty),* $(,)?]
82    ) => {
83        #[derive(Debug, Default)]
84        pub struct $builder_name {
85            $(
86                $req_field: Option<$req_type>,
87            )*
88            $(
89                $opt_field: Option<$opt_type>,
90            )*
91            config: Option<openlark_core::config::Config>,
92            _phantom: std::marker::PhantomData<$request_name>,
93        }
94
95        impl $builder_name {
96            /// 设置配置(链式调用)
97            pub fn with_config(mut self, config: openlark_core::config::Config) -> Self {
98                self.config = Some(config);
99                self
100            }
101
102            $(
103                /// 设置必填字段
104                pub fn $req_field(mut self, value: impl Into<$req_type>) -> Self {
105                    self.$req_field = Some(value.into());
106                    self
107                }
108            )*
109
110            $(
111                /// 设置可选字段
112                pub fn $opt_field(mut self, value: impl Into<$opt_type>) -> Self {
113                    self.$opt_field = Some(value.into());
114                    self
115                }
116            )*
117
118            /// 构建请求实例,验证所有必填字段
119            pub fn build(self) -> openlark_core::SDKResult<$request_name> {
120                // 验证必填字段
121                $(
122                    let $req_field = self.$req_field.ok_or_else(|| {
123                        openlark_core::error::validation_error(
124                            stringify!($req_field),
125                            concat!("必填字段 '", stringify!($req_field), "' 未设置")
126                        )
127                    })?;
128                )*
129
130                let config = self.config.ok_or_else(|| {
131                    openlark_core::error::validation_error(
132                        "config",
133                        "Config 未设置,请使用 with_config() 方法"
134                    )
135                })?;
136
137                Ok($request_name {
138                    $(
139                        $req_field,
140                    )*
141                    $(
142                        $opt_field: self.$opt_field,
143                    )*
144                    config,
145                })
146            }
147        }
148
149        impl $request_name {
150            /// 创建构建器实例
151            pub fn builder() -> $builder_name {
152                $builder_name::default()
153            }
154        }
155    };
156}
157
158/// 简化的流式构建器宏
159///
160/// 为已有的 Request 结构添加 builder() 方法,保持向后兼容。
161///
162/// # 使用场景
163///
164/// 当 Request 结构已经定义,且使用 String::new() 初始化字段时,
165/// 使用此宏添加 Builder 模式支持。
166///
167/// # 示例
168///
169/// ```rust,ignore
170/// // 已有的 Request 结构
171/// #[derive(Debug, Clone)]
172/// pub struct MyRequest {
173///     pub app_token: String,
174///     pub table_id: String,
175///     pub config: Config,
176/// }
177///
178/// // 添加 Builder 支持
179/// impl_fluent_builder!(MyRequest, app_token, table_id);
180/// ```
181#[macro_export]
182macro_rules! impl_fluent_builder {
183    (
184        $request_name:ident,
185        config: Config,
186        required: [$($req_field:ident: $req_type:ty),* $(,)?],
187        optional: [$($opt_field:ident: $opt_type:ty),* $(,)?]
188    ) => {
189        impl $request_name {
190            /// 创建 Builder 实例
191            pub fn builder() -> $request_name {
192                Self {
193                    $(
194                        $req_field: String::new(),
195                    )*
196                    $(
197                        $opt_field: None,
198                    )*
199                    config: Config::default(),
200                }
201            }
202
203            $(
204                /// 设置必填字段(流式接口)
205                pub fn $req_field(mut self, value: impl Into<$req_type>) -> Self {
206                    self.$req_field = value.into();
207                    self
208                }
209            )*
210
211            $(
212                /// 设置可选字段(流式接口)
213                pub fn $opt_field(mut self, value: impl Into<$opt_type>) -> Self {
214                    self.$opt_field = Some(value.into());
215                    self
216                }
217            )*
218        }
219    };
220}
221
222#[cfg(test)]
223mod tests {
224    use openlark_core::config::Config;
225
226    // 测试用的请求结构
227    // 测试 helper,见 #267:impl_required_builder! 宏在 build() 中按字段名 `config` 构造,无法改名。
228    // 用 #[expect(dead_code)] 显式标记:CI grep 只拦 #[allow],#[expect]] 是受控的预期死代码。
229    #[expect(dead_code)]
230    #[derive(Debug, Clone)]
231    pub struct TestRequest {
232        app_token: String,
233        table_id: String,
234        user_id_type: Option<String>,
235        config: Config,
236    }
237
238    // 使用宏生成 Builder
239    impl_required_builder!(
240        TestRequest,
241        TestRequestBuilder,
242        required: [
243            app_token: String,
244            table_id: String
245        ],
246        optional: [
247            user_id_type: String
248        ]
249    );
250
251    #[test]
252    fn test_required_builder_success() {
253        let config = Config::builder().app_id("test").app_secret("test").build();
254
255        let request = TestRequest::builder()
256            .with_config(config)
257            .app_token("token")
258            .table_id("table")
259            .build()
260            .unwrap();
261
262        assert_eq!(request.app_token, "token");
263        assert_eq!(request.table_id, "table");
264        assert!(request.user_id_type.is_none());
265    }
266
267    #[test]
268    fn test_required_builder_with_optional() {
269        let config = Config::builder().app_id("test").app_secret("test").build();
270
271        let request = TestRequest::builder()
272            .with_config(config)
273            .app_token("token")
274            .table_id("table")
275            .user_id_type("open_id")
276            .build()
277            .unwrap();
278
279        assert_eq!(request.user_id_type, Some("open_id".to_string()));
280    }
281
282    #[test]
283    fn test_required_builder_missing_required_field() {
284        let config = Config::builder().app_id("test").app_secret("test").build();
285
286        let result = TestRequest::builder()
287            .with_config(config)
288            .app_token("token")
289            // 缺少 table_id
290            .build();
291
292        assert!(result.is_err());
293        assert!(result.unwrap_err().to_string().contains("table_id"));
294    }
295
296    #[test]
297    fn test_required_builder_missing_config() {
298        let result = TestRequest::builder()
299            .app_token("token")
300            .table_id("table")
301            // 缺少 config
302            .build();
303
304        assert!(result.is_err());
305        assert!(result.unwrap_err().to_string().contains("Config"));
306    }
307
308    #[test]
309    fn test_required_builder_into_string() {
310        let config = Config::builder().app_id("test").app_secret("test").build();
311
312        // 测试 impl Into<String>
313        let request = TestRequest::builder()
314            .with_config(config)
315            .app_token("token") // &str
316            .table_id(String::from("table")) // String
317            .build()
318            .unwrap();
319
320        assert_eq!(request.app_token, "token");
321        assert_eq!(request.table_id, "table");
322    }
323}