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}