Skip to main content

openlark_core/
constants.rs

1use std::fmt::Display;
2
3/// 应用类型
4#[derive(Default, Hash, Eq, PartialEq, Debug, Copy, Clone)]
5pub enum AppType {
6    /// 自建应用
7    #[default]
8    SelfBuild,
9    /// 商店应用
10    Marketplace,
11}
12
13impl AppType {
14    /// 获取应用类型字符串
15    pub fn as_str(&self) -> &'static str {
16        match self {
17            AppType::SelfBuild => "self_build",
18            AppType::Marketplace => "marketplace",
19        }
20    }
21}
22
23impl Display for AppType {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        write!(f, "{}", self.as_str())
26    }
27}
28
29/// 飞书 API 路径前缀
30pub const API_PATH_PREFIX: &str = "/open-apis/";
31/// 应用凭证内部接口 URL 路径
32pub const APP_ACCESS_TOKEN_INTERNAL_URL_PATH: &str = "/open-apis/auth/v3/app_access_token/internal";
33/// 应用凭证 URL 路径
34pub const APP_ACCESS_TOKEN_URL_PATH: &str = "/open-apis/auth/v3/app_access_token";
35/// 租户凭证内部接口 URL 路径
36pub const TENANT_ACCESS_TOKEN_INTERNAL_URL_PATH: &str =
37    "/open-apis/auth/v3/tenant_access_token/internal";
38/// 租户凭证 URL 路径
39pub const TENANT_ACCESS_TOKEN_URL_PATH: &str = "/open-apis/auth/v3/tenant_access_token";
40/// 申请应用票据 URL 路径
41pub const APPLY_APP_TICKET_PATH: &str = "/open-apis/auth/v3/app_ticket/resend";
42
43#[derive(Default, Hash, Eq, PartialEq, Debug, Copy, Clone)]
44/// 访问令牌类型
45///
46/// 定义不同的访问令牌类型,用于API认证
47pub enum AccessTokenType {
48    /// 无访问令牌
49    #[default]
50    None,
51    /// 应用访问令牌
52    App,
53    /// 租户访问令牌
54    Tenant,
55    /// 用户访问令牌
56    User,
57}
58
59impl AccessTokenType {
60    /// 获取访问令牌类型字符串
61    pub fn as_str(&self) -> &'static str {
62        match self {
63            AccessTokenType::None => "none_access_token",
64            AccessTokenType::App => "app_access_token",
65            AccessTokenType::Tenant => "tenant_access_token",
66            AccessTokenType::User => "user_access_token",
67        }
68    }
69}
70
71impl Display for AccessTokenType {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        write!(f, "{}", self.as_str())
74    }
75}
76
77/// 项目名称
78pub const PROJECT: &str = "open-lark";
79/// 项目版本号(来自 Cargo.toml)
80pub const VERSION: &str = env!("CARGO_PKG_VERSION");
81
82/// Domain
83pub const FEISHU_BASE_URL: &str = "https://open.feishu.cn";
84/// 飞书国际版 Lark 域名
85pub const LARK_BASE_URL: &str = "https://open.larksuite.com";
86
87/// 默认 Content-Type(JSON 编码)
88pub const DEFAULT_CONTENT_TYPE: &str = "application/json; charset=utf-8";
89/// 文件上传 Content-Type
90pub const FILE_CONTENT_TYPE: &str = "multipart/form-data";
91/// User-Agent HTTP 头名称
92pub const USER_AGENT_HEADER: &str = "User-Agent";
93
94/// X-Request-Id HTTP 请求头键名
95pub const HTTP_HEADER_KEY_REQUEST_ID: &str = "X-Request-Id";
96/// Request-Id HTTP 请求头键名
97pub const HTTP_HEADER_REQUEST_ID: &str = "Request-Id";
98
99/// X-Tt-Logid HTTP 请求头键名
100pub const HTTP_HEADER_KEY_LOG_ID: &str = "X-Tt-Logid";
101/// Content-Type HTTP 头名称
102pub const CONTENT_TYPE_HEADER: &str = "Content-Type";
103/// JSON Content-Type 值
104pub const CONTENT_TYPE_JSON: &str = "application/json";
105/// 自定义请求 ID 头名称
106pub const CUSTOM_REQUEST_ID: &str = "Open-Lark-Request-Id";
107/// 应用票据缓存键前缀
108pub const APP_TICKET_KEY_PREFIX: &str = "app_ticket";
109/// 应用访问令牌缓存键前缀
110pub const APP_ACCESS_TOKEN_KEY_PREFIX: &str = "app_access_token";
111/// 租户访问令牌缓存键前缀
112pub const TENANT_ACCESS_TOKEN_KEY_PREFIX: &str = "tenant_access_token";
113/// 令牌过期时间增量(秒),用于提前刷新令牌
114pub const EXPIRY_DELTA: i32 = 60 * 3;
115/// 应用票据无效错误码
116pub const ERR_CODE_APP_TICKET_INVALID: i32 = 10012;
117/// 访问令牌无效错误码
118pub const ERR_CODE_ACCESS_TOKEN_INVALID: i32 = 99991671;
119/// 应用访问令牌无效错误码
120pub const ERR_CODE_APP_ACCESS_TOKEN_INVALID: i32 = 99991664;
121/// 租户访问令牌无效错误码
122pub const ERR_CODE_TENANT_ACCESS_TOKEN_INVALID: i32 = 99991663;
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_app_type_default() {
130        assert_eq!(AppType::default(), AppType::SelfBuild);
131    }
132
133    #[test]
134    fn test_app_type_debug_clone() {
135        let app_type = AppType::Marketplace;
136        let cloned = app_type;
137        assert_eq!(format!("{:?}", app_type), "Marketplace");
138        assert_eq!(cloned, AppType::Marketplace);
139    }
140
141    #[test]
142    fn test_app_type_equality() {
143        assert_eq!(AppType::SelfBuild, AppType::SelfBuild);
144        assert_eq!(AppType::Marketplace, AppType::Marketplace);
145        assert_ne!(AppType::SelfBuild, AppType::Marketplace);
146    }
147
148    #[test]
149    fn test_access_token_type_default() {
150        assert_eq!(AccessTokenType::default(), AccessTokenType::None);
151    }
152
153    #[test]
154    fn test_access_token_type_display() {
155        assert_eq!(AccessTokenType::None.to_string(), "none_access_token");
156        assert_eq!(AccessTokenType::App.to_string(), "app_access_token");
157        assert_eq!(AccessTokenType::Tenant.to_string(), "tenant_access_token");
158        assert_eq!(AccessTokenType::User.to_string(), "user_access_token");
159    }
160
161    #[test]
162    fn test_access_token_type_debug_clone() {
163        let token_type = AccessTokenType::App;
164        let cloned = token_type;
165        assert_eq!(format!("{:?}", token_type), "App");
166        assert_eq!(cloned, AccessTokenType::App);
167    }
168
169    #[test]
170    fn test_access_token_type_equality() {
171        assert_eq!(AccessTokenType::None, AccessTokenType::None);
172        assert_eq!(AccessTokenType::App, AccessTokenType::App);
173        assert_eq!(AccessTokenType::Tenant, AccessTokenType::Tenant);
174        assert_eq!(AccessTokenType::User, AccessTokenType::User);
175        assert_ne!(AccessTokenType::App, AccessTokenType::Tenant);
176    }
177
178    #[test]
179    fn test_constants_values() {
180        // Test URL paths
181        assert_eq!(
182            APP_ACCESS_TOKEN_INTERNAL_URL_PATH,
183            "/open-apis/auth/v3/app_access_token/internal"
184        );
185        assert_eq!(
186            APP_ACCESS_TOKEN_URL_PATH,
187            "/open-apis/auth/v3/app_access_token"
188        );
189        assert_eq!(
190            TENANT_ACCESS_TOKEN_INTERNAL_URL_PATH,
191            "/open-apis/auth/v3/tenant_access_token/internal"
192        );
193        assert_eq!(
194            TENANT_ACCESS_TOKEN_URL_PATH,
195            "/open-apis/auth/v3/tenant_access_token"
196        );
197        assert_eq!(
198            APPLY_APP_TICKET_PATH,
199            "/open-apis/auth/v3/app_ticket/resend"
200        );
201
202        // Test project info
203        assert_eq!(PROJECT, "open-lark");
204        // VERSION is a const from env!() so it's never empty - verify it's a valid version
205        assert!(
206            VERSION.contains('.'),
207            "Version should contain dots for proper semver"
208        );
209
210        // Test base URLs
211        assert_eq!(FEISHU_BASE_URL, "https://open.feishu.cn");
212        assert_eq!(LARK_BASE_URL, "https://open.larksuite.com");
213
214        // Test content types
215        assert_eq!(DEFAULT_CONTENT_TYPE, "application/json; charset=utf-8");
216        assert_eq!(FILE_CONTENT_TYPE, "multipart/form-data");
217        assert_eq!(CONTENT_TYPE_JSON, "application/json");
218
219        // Test headers
220        assert_eq!(USER_AGENT_HEADER, "User-Agent");
221        assert_eq!(HTTP_HEADER_KEY_REQUEST_ID, "X-Request-Id");
222        assert_eq!(HTTP_HEADER_REQUEST_ID, "Request-Id");
223        assert_eq!(HTTP_HEADER_KEY_LOG_ID, "X-Tt-Logid");
224        assert_eq!(CONTENT_TYPE_HEADER, "Content-Type");
225        assert_eq!(CUSTOM_REQUEST_ID, "Open-Lark-Request-Id");
226
227        // Test cache key prefixes
228        assert_eq!(APP_TICKET_KEY_PREFIX, "app_ticket");
229        assert_eq!(APP_ACCESS_TOKEN_KEY_PREFIX, "app_access_token");
230        assert_eq!(TENANT_ACCESS_TOKEN_KEY_PREFIX, "tenant_access_token");
231
232        // Test numeric constants
233        assert_eq!(EXPIRY_DELTA, 60 * 3);
234        assert_eq!(ERR_CODE_APP_TICKET_INVALID, 10012);
235        assert_eq!(ERR_CODE_ACCESS_TOKEN_INVALID, 99991671);
236        assert_eq!(ERR_CODE_APP_ACCESS_TOKEN_INVALID, 99991664);
237        assert_eq!(ERR_CODE_TENANT_ACCESS_TOKEN_INVALID, 99991663);
238    }
239
240    #[test]
241    fn test_version_format() {
242        // VERSION should follow semver format
243        let version_parts: Vec<&str> = VERSION.split('.').collect();
244        assert!(
245            version_parts.len() >= 2,
246            "Version should have at least major.minor format"
247        );
248
249        // Each part should be numeric
250        for part in &version_parts {
251            assert!(!part.is_empty(), "Version parts should not be empty");
252            // Basic check for digits (might have pre-release suffixes)
253            assert!(
254                part.chars().next().unwrap().is_ascii_digit(),
255                "Version should start with digit"
256            );
257        }
258    }
259
260    #[test]
261    fn test_url_constants_format() {
262        // All URLs should start with proper prefixes
263        assert!(FEISHU_BASE_URL.starts_with("https://"));
264        assert!(LARK_BASE_URL.starts_with("https://"));
265
266        // API paths should start with /open-apis/
267        assert!(APP_ACCESS_TOKEN_INTERNAL_URL_PATH.starts_with("/open-apis/"));
268        assert!(APP_ACCESS_TOKEN_URL_PATH.starts_with("/open-apis/"));
269        assert!(TENANT_ACCESS_TOKEN_INTERNAL_URL_PATH.starts_with("/open-apis/"));
270        assert!(TENANT_ACCESS_TOKEN_URL_PATH.starts_with("/open-apis/"));
271        assert!(APPLY_APP_TICKET_PATH.starts_with("/open-apis/"));
272    }
273
274    #[test]
275    fn test_error_code_ranges() {
276        // Error codes should be in expected ranges
277        // These are const assertions that will be optimized out
278        // Removed to avoid clippy warnings about constant assertions
279
280        // Different error codes should be unique
281        let error_codes = [
282            ERR_CODE_APP_TICKET_INVALID,
283            ERR_CODE_ACCESS_TOKEN_INVALID,
284            ERR_CODE_APP_ACCESS_TOKEN_INVALID,
285            ERR_CODE_TENANT_ACCESS_TOKEN_INVALID,
286        ];
287
288        for (i, &code1) in error_codes.iter().enumerate() {
289            for &code2 in error_codes.iter().skip(i + 1) {
290                assert_ne!(code1, code2, "Error codes should be unique");
291            }
292        }
293    }
294
295    #[test]
296    fn test_expiry_delta_reasonable() {
297        // EXPIRY_DELTA should be reasonable (3 minutes in seconds)
298        assert_eq!(EXPIRY_DELTA, 180);
299        // These are const assertions that will be optimized out
300        // Removed to avoid clippy warnings about constant assertions
301    }
302
303    #[test]
304    fn test_content_type_formats() {
305        assert!(DEFAULT_CONTENT_TYPE.contains("application/json"));
306        assert!(DEFAULT_CONTENT_TYPE.contains("charset=utf-8"));
307        assert!(FILE_CONTENT_TYPE.contains("multipart/form-data"));
308        assert_eq!(CONTENT_TYPE_JSON, "application/json");
309    }
310}