1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Serialize, Deserialize)]
10pub struct AppManifest {
11 #[serde(rename = "_metadata")]
12 pub _metadata: Metadata,
13 pub display_information: DisplayInformation,
14 pub features: Features,
15 pub oauth_config: OAuthConfig,
16 pub settings: Settings,
17}
18
19#[derive(Debug, Serialize, Deserialize)]
20pub struct Metadata {
21 pub major_version: u32,
22 pub minor_version: u32,
23}
24
25#[derive(Debug, Serialize, Deserialize)]
26pub struct DisplayInformation {
27 pub name: String,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub description: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub background_color: Option<String>,
32}
33
34#[derive(Debug, Serialize, Deserialize)]
35pub struct Features {
36 pub bot_user: BotUser,
37}
38
39#[derive(Debug, Serialize, Deserialize)]
40pub struct BotUser {
41 pub display_name: String,
42 pub always_online: bool,
43}
44
45#[derive(Debug, Serialize, Deserialize)]
46pub struct OAuthConfig {
47 pub redirect_urls: Vec<String>,
48 pub scopes: Scopes,
49}
50
51#[derive(Debug, Serialize, Deserialize)]
52pub struct Scopes {
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub bot: Option<Vec<String>>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub user: Option<Vec<String>>,
57}
58
59#[derive(Debug, Serialize, Deserialize)]
60pub struct Settings {
61 pub org_deploy_enabled: bool,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub socket_mode_enabled: Option<bool>,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub token_rotation_enabled: Option<bool>,
66}
67
68pub fn generate_manifest(
82 _client_id: &str,
83 bot_scopes: &[String],
84 user_scopes: &[String],
85 redirect_uri: &str,
86 _use_cloudflared: bool,
87 _use_ngrok: bool,
88 profile_name: &str,
89) -> Result<String, String> {
90 let redirect_urls = vec![redirect_uri.to_string()];
93
94 let manifest = AppManifest {
95 _metadata: Metadata {
96 major_version: 2,
97 minor_version: 1,
98 },
99 display_information: DisplayInformation {
100 name: format!("slack-rs ({})", profile_name),
101 description: Some(format!(
102 "Slack CLI application for profile '{}'",
103 profile_name
104 )),
105 background_color: Some("#2c2d30".to_string()),
106 },
107 features: Features {
108 bot_user: BotUser {
109 display_name: format!("slack-rs-{}", profile_name),
110 always_online: false,
111 },
112 },
113 oauth_config: OAuthConfig {
114 redirect_urls,
115 scopes: Scopes {
116 bot: if bot_scopes.is_empty() {
117 None
118 } else {
119 Some(bot_scopes.to_vec())
120 },
121 user: if user_scopes.is_empty() {
122 None
123 } else {
124 Some(user_scopes.to_vec())
125 },
126 },
127 },
128 settings: Settings {
129 org_deploy_enabled: false,
130 socket_mode_enabled: Some(false),
131 token_rotation_enabled: Some(false),
132 },
133 };
134
135 let yaml_string = serde_yaml::to_string(&manifest)
137 .map_err(|e| format!("Failed to serialize manifest: {}", e))?;
138
139 if !yaml_string.starts_with("_metadata:") && !yaml_string.starts_with("\"_metadata\":") {
141 return Err(format!(
142 "Generated YAML does not start with _metadata field. First line: {}",
143 yaml_string.lines().next().unwrap_or("(empty)")
144 ));
145 }
146
147 Ok(yaml_string)
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_generate_manifest_with_bot_scopes_only() {
156 let bot_scopes = vec!["chat:write".to_string(), "users:read".to_string()];
157 let user_scopes = vec![];
158 let result = generate_manifest(
159 "test-client-id",
160 &bot_scopes,
161 &user_scopes,
162 "http://localhost:8765/callback",
163 false,
164 false,
165 "default",
166 );
167
168 assert!(result.is_ok());
169 let yaml = result.unwrap();
170
171 println!("Generated YAML:\n{}", yaml);
173
174 assert!(yaml.contains("_metadata:"));
176 assert!(yaml.contains("major_version: 2"));
177 assert!(yaml.contains("minor_version: 1"));
178 assert!(yaml.contains("display_information:"));
179 assert!(yaml.contains("features:"));
180 assert!(yaml.contains("oauth_config:"));
181 assert!(yaml.contains("settings:"));
182
183 assert!(yaml.contains("chat:write"));
185 assert!(yaml.contains("users:read"));
186 assert!(yaml.contains("http://localhost:8765/callback"));
187 assert!(yaml.contains("slack-rs (default)"));
188 assert!(yaml.contains("bot:"));
189 assert!(yaml.contains("scopes:"));
190
191 let parsed: Result<AppManifest, _> = serde_yaml::from_str(&yaml);
193 assert!(
194 parsed.is_ok(),
195 "Generated YAML should be valid and parseable"
196 );
197 }
198
199 #[test]
200 fn test_generate_manifest_with_cloudflared() {
201 let bot_scopes = vec!["chat:write".to_string()];
202 let user_scopes = vec!["search:read".to_string()];
203 let result = generate_manifest(
204 "test-client-id",
205 &bot_scopes,
206 &user_scopes,
207 "http://localhost:8765/callback",
208 true,
209 false,
210 "work",
211 );
212
213 assert!(result.is_ok());
214 let yaml = result.unwrap();
215 assert!(yaml.contains("http://localhost:8765/callback"));
217 assert!(yaml.contains("chat:write"));
218 assert!(yaml.contains("search:read"));
219 }
220
221 #[test]
222 fn test_generate_manifest_with_user_scopes() {
223 let bot_scopes = vec!["chat:write".to_string()];
224 let user_scopes = vec!["users:read".to_string(), "search:read".to_string()];
225 let result = generate_manifest(
226 "test-client-id",
227 &bot_scopes,
228 &user_scopes,
229 "http://localhost:8765/callback",
230 false,
231 false,
232 "personal",
233 );
234
235 assert!(result.is_ok());
236 let yaml = result.unwrap();
237 assert!(yaml.contains("chat:write"));
238 assert!(yaml.contains("users:read"));
239 assert!(yaml.contains("search:read"));
240 assert!(yaml.contains("bot:"));
241 assert!(yaml.contains("user:"));
242 }
243
244 #[test]
245 fn test_generate_manifest_empty_scopes() {
246 let bot_scopes = vec![];
247 let user_scopes = vec![];
248 let result = generate_manifest(
249 "test-client-id",
250 &bot_scopes,
251 &user_scopes,
252 "http://localhost:8765/callback",
253 false,
254 false,
255 "empty",
256 );
257
258 assert!(result.is_ok());
260 }
261
262 #[test]
263 fn test_generate_manifest_with_ngrok() {
264 let bot_scopes = vec!["chat:write".to_string()];
265 let user_scopes = vec!["search:read".to_string()];
266 let result = generate_manifest(
267 "test-client-id",
268 &bot_scopes,
269 &user_scopes,
270 "http://localhost:8765/callback",
271 false,
272 true,
273 "ngrok-test",
274 );
275
276 assert!(result.is_ok());
277 let yaml = result.unwrap();
278 assert!(yaml.contains("http://localhost:8765/callback"));
280 assert!(yaml.contains("chat:write"));
281 assert!(yaml.contains("search:read"));
282 }
283}