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