tencentcloud_sms/
config.rs

1use std::time::Duration;
2
3/// 腾讯云服务地域
4///
5/// 该枚举定义了腾讯云支持的所有地域,用于指定 `X-TC-Region` 请求头。
6/// 与官方 SDK 保持一致。
7///
8/// # 国内地域
9/// - `ApGuangzhou` - 广州(默认)
10/// - `ApBeijing` - 北京
11/// - `ApNanjing` - 南京
12/// - `ApShanghai` - 上海
13/// - `ApChengdu` - 成都
14/// - `ApChongqing` - 重庆
15/// - `ApHongkong` - 香港
16///
17/// # 金融地域
18/// 金融地域与普通地域隔离,使用 `Endpoint::Region` 时会自动使用金融区域名。
19/// - `ApShanghaiFsi` - 上海金融
20/// - `ApShenzhenFsi` - 深圳金融
21///
22/// # 国际地域
23/// - `ApSingapore` - 新加坡
24/// - `ApBangkok` - 曼谷
25/// - `ApTokyo` - 东京
26/// - `ApSeoul` - 首尔
27/// - `ApMumbai` - 孟买
28/// - `EuFrankfurt` - 法兰克福
29/// - `EuMoscow` - 莫斯科
30/// - `NaSiliconvalley` - 硅谷
31/// - `NaAshburn` - 弗吉尼亚
32/// - `NaToronto` - 多伦多
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum Region {
35    // ========== 国内地域 ==========
36    /// 广州(默认)
37    #[default]
38    ApGuangzhou,
39    /// 北京
40    ApBeijing,
41    /// 南京
42    ApNanjing,
43    /// 上海
44    ApShanghai,
45    /// 成都
46    ApChengdu,
47    /// 重庆
48    ApChongqing,
49    /// 香港
50    ApHongkong,
51
52    // ========== 金融地域 ==========
53    /// 上海金融
54    ApShanghaiFsi,
55    /// 深圳金融
56    ApShenzhenFsi,
57
58    // ========== 国际地域 ==========
59    /// 新加坡
60    ApSingapore,
61    /// 曼谷
62    ApBangkok,
63    /// 东京
64    ApTokyo,
65    /// 首尔
66    ApSeoul,
67    /// 孟买
68    ApMumbai,
69    /// 法兰克福
70    EuFrankfurt,
71    /// 莫斯科
72    EuMoscow,
73    /// 硅谷
74    NaSiliconvalley,
75    /// 弗吉尼亚
76    NaAshburn,
77    /// 多伦多
78    NaToronto,
79}
80
81impl Region {
82    /// 返回地域代码字符串,用于 X-TC-Region 请求头
83    pub fn as_str(&self) -> &'static str {
84        match self {
85            // 国内
86            Self::ApGuangzhou => "ap-guangzhou",
87            Self::ApBeijing => "ap-beijing",
88            Self::ApNanjing => "ap-nanjing",
89            Self::ApShanghai => "ap-shanghai",
90            Self::ApChengdu => "ap-chengdu",
91            Self::ApChongqing => "ap-chongqing",
92            Self::ApHongkong => "ap-hongkong",
93            // 金融
94            Self::ApShanghaiFsi => "ap-shanghai-fsi",
95            Self::ApShenzhenFsi => "ap-shenzhen-fsi",
96            // 国际
97            Self::ApSingapore => "ap-singapore",
98            Self::ApBangkok => "ap-bangkok",
99            Self::ApTokyo => "ap-tokyo",
100            Self::ApSeoul => "ap-seoul",
101            Self::ApMumbai => "ap-mumbai",
102            Self::EuFrankfurt => "eu-frankfurt",
103            Self::EuMoscow => "eu-moscow",
104            Self::NaSiliconvalley => "na-siliconvalley",
105            Self::NaAshburn => "na-ashburn",
106            Self::NaToronto => "na-toronto",
107        }
108    }
109
110    /// 判断是否为金融地域
111    pub fn is_financial(&self) -> bool {
112        matches!(self, Self::ApShanghaiFsi | Self::ApShenzhenFsi)
113    }
114
115    /// 返回地域对应的 SMS API 域名
116    pub fn host(&self) -> &'static str {
117        match self {
118            // 国内
119            Self::ApGuangzhou => "sms.ap-guangzhou.tencentcloudapi.com",
120            Self::ApBeijing => "sms.ap-beijing.tencentcloudapi.com",
121            Self::ApNanjing => "sms.ap-nanjing.tencentcloudapi.com",
122            Self::ApShanghai => "sms.ap-shanghai.tencentcloudapi.com",
123            Self::ApChengdu => "sms.ap-chengdu.tencentcloudapi.com",
124            Self::ApChongqing => "sms.ap-chongqing.tencentcloudapi.com",
125            Self::ApHongkong => "sms.ap-hongkong.tencentcloudapi.com",
126            // 金融
127            Self::ApShanghaiFsi => "sms.ap-shanghai-fsi.tencentcloudapi.com",
128            Self::ApShenzhenFsi => "sms.ap-shenzhen-fsi.tencentcloudapi.com",
129            // 国际
130            Self::ApSingapore => "sms.ap-singapore.tencentcloudapi.com",
131            Self::ApBangkok => "sms.ap-bangkok.tencentcloudapi.com",
132            Self::ApTokyo => "sms.ap-tokyo.tencentcloudapi.com",
133            Self::ApSeoul => "sms.ap-seoul.tencentcloudapi.com",
134            Self::ApMumbai => "sms.ap-mumbai.tencentcloudapi.com",
135            Self::EuFrankfurt => "sms.eu-frankfurt.tencentcloudapi.com",
136            Self::EuMoscow => "sms.eu-moscow.tencentcloudapi.com",
137            Self::NaSiliconvalley => "sms.na-siliconvalley.tencentcloudapi.com",
138            Self::NaAshburn => "sms.na-ashburn.tencentcloudapi.com",
139            Self::NaToronto => "sms.na-toronto.tencentcloudapi.com",
140        }
141    }
142}
143
144/// API 接入点配置
145///
146/// 定义 HTTP 请求发送到的域名。
147///
148/// # 接入点与地域的区别
149/// - **接入点 (Endpoint)**: 网络入口,决定 HTTP 请求发送到哪个服务器
150/// - **地域 (Region)**: 服务地域,通过 `X-TC-Region` 头指定,决定数据处理位置
151///
152/// # 各选项说明
153/// - `Nearest`: 就近接入,使用 `sms.tencentcloudapi.com`,系统自动路由到最近服务器
154/// - `Region`: 使用 `ClientConfig::region` 对应的地域接入点
155/// - `International`: 国际短信接入点 `sms.intl.tencentcloudapi.com`
156/// - `Custom`: 自定义域名
157///
158/// # 金融地域注意
159/// 金融地域必须使用 `Region` 接入点。
160#[derive(Debug, Clone, PartialEq, Eq, Default)]
161pub enum Endpoint {
162    /// 就近接入点 `sms.tencentcloudapi.com`(默认)
163    #[default]
164    Nearest,
165    /// 使用 `ClientConfig::region` 对应的地域接入点
166    Region,
167    /// 国际短信接入点 `sms.intl.tencentcloudapi.com`
168    International,
169    /// 自定义域名(高级用法)
170    Custom(String),
171}
172
173impl Endpoint {
174    /// 根据地域获取接入点域名
175    pub fn host<'a>(&'a self, region: &Region) -> &'a str {
176        match self {
177            Self::Nearest => "sms.tencentcloudapi.com",
178            // Region::host() 返回 &'static str,满足 'a 的要求
179            Self::Region => region.host(),
180            Self::International => "sms.intl.tencentcloudapi.com",
181            Self::Custom(host) => host.as_str(),
182        }
183    }
184}
185
186#[derive(Debug, Clone, typed_builder::TypedBuilder)]
187pub struct RetryConfig {
188    #[builder(default = 3)]
189    pub max_retries: u64,
190    #[builder(default = Duration::from_millis(100))]
191    pub initial_backoff: Duration,
192    #[builder(default = Duration::from_secs(10))]
193    pub max_backoff: Duration,
194    #[builder(default = 2.0)]
195    pub backoff_multiplier: f64,
196    #[builder(default = true)]
197    pub add_jitter: bool,
198}
199
200impl Default for RetryConfig {
201    fn default() -> Self {
202        Self::builder().build()
203    }
204}
205
206impl RetryConfig {
207    /// 创建一个新的默认重试配置
208    pub fn new() -> Self {
209        Self::default()
210    }
211}
212
213/// 客户端配置
214///
215/// # 示例
216///
217/// ```
218/// use tencentcloud_sms::{ClientConfig, Region, Endpoint};
219///
220/// // 默认配置(广州地域,就近接入)
221/// let config = ClientConfig::default();
222///
223/// // 指定地域
224/// let config = ClientConfig::builder()
225///     .region(Region::ApBeijing)
226///     .build();
227///
228/// // 金融地域(需使用 Region 接入点)
229/// let config = ClientConfig::builder()
230///     .region(Region::ApShanghaiFsi)
231///     .endpoint(Endpoint::Region)
232///     .build();
233///
234/// // 国际短信
235/// let config = ClientConfig::builder()
236///     .endpoint(Endpoint::International)
237///     .build();
238/// ```
239#[derive(Debug, Clone, typed_builder::TypedBuilder)]
240pub struct ClientConfig {
241    /// 服务地域
242    ///
243    /// 指定 API 的服务地域,用于 `X-TC-Region` 请求头。
244    /// 默认为广州 (`ApGuangzhou`)。
245    #[builder(default)]
246    pub region: Region,
247
248    /// 接入点配置
249    ///
250    /// - `Nearest`: 就近接入(默认)
251    /// - `Region(region)`: 指定地域接入点
252    /// - `International`: 国际短信接入点
253    /// - `Custom(host)`: 自定义域名
254    #[builder(default)]
255    pub endpoint: Endpoint,
256
257    /// 请求超时时间
258    #[builder(default = Duration::from_secs(30))]
259    pub timeout: Duration,
260
261    /// 重试配置
262    #[builder(default = Some(RetryConfig::default()))]
263    pub retry: Option<RetryConfig>,
264
265    /// 代理服务器地址
266    #[builder(default)]
267    pub proxy: Option<String>,
268
269    /// 是否使用 HTTPS
270    #[builder(default = true)]
271    pub is_https: bool,
272}
273
274impl Default for ClientConfig {
275    fn default() -> Self {
276        Self::builder().build()
277    }
278}
279
280impl ClientConfig {
281    /// 创建默认配置
282    pub fn new() -> Self {
283        Self::default()
284    }
285
286    /// 验证配置是否合法
287    ///
288    /// # 错误
289    /// - 超时时间为 0
290    /// - 金融地域未使用 Region 接入点
291    /// - 重试配置参数无效
292    pub fn validate(&self) -> crate::SmsResult<()> {
293        // 验证超时时间
294        if self.timeout.as_secs() == 0 && self.timeout.subsec_nanos() == 0 {
295            return Err(crate::SmsError::Configuration(
296                "超时时间不能为 0".to_string(),
297            ));
298        }
299
300        // 验证金融地域必须使用 Region 接入点
301        if self.region.is_financial() && !matches!(self.endpoint, Endpoint::Region) {
302            return Err(crate::SmsError::Configuration(format!(
303                "金融地域 {:?} 必须使用 Endpoint::Region 接入点,当前使用的是 {:?}",
304                self.region, self.endpoint
305            )));
306        }
307
308        // 验证重试配置
309        if let Some(retry) = &self.retry {
310            if retry.max_retries == 0 {
311                return Err(crate::SmsError::Configuration(
312                    "最大重试次数至少为 1".to_string(),
313                ));
314            }
315
316            if retry.initial_backoff.as_secs() == 0 && retry.initial_backoff.subsec_nanos() == 0 {
317                return Err(crate::SmsError::Configuration(
318                    "初始退避时间不能为 0".to_string(),
319                ));
320            }
321
322            if retry.max_backoff < retry.initial_backoff {
323                return Err(crate::SmsError::Configuration(
324                    "最大退避时间不能小于初始退避时间".to_string(),
325                ));
326            }
327
328            if retry.backoff_multiplier < 1.0 {
329                return Err(crate::SmsError::Configuration(
330                    "退避倍数必须 >= 1.0".to_string(),
331                ));
332            }
333        }
334
335        Ok(())
336    }
337
338    /// 使用指定地域创建配置
339    ///
340    /// # 示例
341    /// ```
342    /// use tencentcloud_sms::{ClientConfig, Region};
343    ///
344    /// let config = ClientConfig::with_region(Region::ApBeijing);
345    /// ```
346    pub fn with_region(region: Region) -> Self {
347        Self::builder().region(region).build()
348    }
349
350    /// 使用上海金融地域创建配置
351    ///
352    /// 自动设置 Region 为上海金融区,Endpoint 为 Region 接入点。
353    ///
354    /// # 示例
355    /// ```
356    /// use tencentcloud_sms::ClientConfig;
357    ///
358    /// let config = ClientConfig::financial_shanghai();
359    /// ```
360    pub fn financial_shanghai() -> Self {
361        Self::builder()
362            .region(Region::ApShanghaiFsi)
363            .endpoint(Endpoint::Region)
364            .build()
365    }
366
367    /// 使用深圳金融地域创建配置
368    ///
369    /// 自动设置 Region 为深圳金融区,Endpoint 为 Region 接入点。
370    ///
371    /// # 示例
372    /// ```
373    /// use tencentcloud_sms::ClientConfig;
374    ///
375    /// let config = ClientConfig::financial_shenzhen();
376    /// ```
377    pub fn financial_shenzhen() -> Self {
378        Self::builder()
379            .region(Region::ApShenzhenFsi)
380            .endpoint(Endpoint::Region)
381            .build()
382    }
383
384    /// 使用国际短信接入点创建配置
385    ///
386    /// # 示例
387    /// ```
388    /// use tencentcloud_sms::ClientConfig;
389    ///
390    /// let config = ClientConfig::international();
391    /// ```
392    pub fn international() -> Self {
393        Self::builder().endpoint(Endpoint::International).build()
394    }
395
396    /// 自定义超时时间
397    ///
398    /// # 示例
399    /// ```
400    /// use std::time::Duration;
401    /// use tencentcloud_sms::ClientConfig;
402    ///
403    /// let config = ClientConfig::with_timeout(Duration::from_secs(60));
404    /// ```
405    pub fn with_timeout(timeout: Duration) -> Self {
406        Self::builder().timeout(timeout).build()
407    }
408
409    /// 禁用重试
410    ///
411    /// # 示例
412    /// ```
413    /// use tencentcloud_sms::ClientConfig;
414    ///
415    /// let config = ClientConfig::without_retry();
416    /// ```
417    pub fn without_retry() -> Self {
418        Self::builder().retry(None).build()
419    }
420
421    /// 自定义重试配置
422    ///
423    /// # 示例
424    /// ```
425    /// use tencentcloud_sms::{ClientConfig, RetryConfig};
426    ///
427    /// let retry_config = RetryConfig::builder()
428    ///     .max_retries(5)
429    ///     .build();
430    /// let config = ClientConfig::with_retry(retry_config);
431    /// ```
432    pub fn with_retry(retry: RetryConfig) -> Self {
433        Self::builder().retry(Some(retry)).build()
434    }
435
436    /// 获取实际使用的域名
437    pub fn host(&self) -> &str {
438        self.endpoint.host(&self.region)
439    }
440}