1use anyhow::Result;
8use qa_spec::{FormSpec, QuestionSpec};
9use serde_json::{Map as JsonMap, Value};
10use std::collections::HashMap;
11
12use crate::qa::prompts::ask_form_spec_question;
13use crate::setup_to_formspec;
14
15pub const SHARED_QUESTION_IDS: &[&str] = &[
20 "public_base_url",
21 ];
24
25#[derive(Clone)]
27pub struct ProviderFormSpec {
28 pub provider_id: String,
30 pub form_spec: FormSpec,
32}
33
34#[derive(Clone, Default)]
36pub struct SharedQuestionsResult {
37 pub shared_questions: Vec<QuestionSpec>,
40 pub question_providers: HashMap<String, Vec<String>>,
42}
43
44pub fn collect_shared_questions(providers: &[ProviderFormSpec]) -> SharedQuestionsResult {
53 if providers.len() <= 1 {
54 return SharedQuestionsResult::default();
55 }
56
57 let mut question_count: HashMap<String, Vec<String>> = HashMap::new();
59 let mut first_question: HashMap<String, QuestionSpec> = HashMap::new();
60
61 for provider in providers {
62 for question in &provider.form_spec.questions {
63 if question.id.is_empty() {
64 continue;
65 }
66 question_count
67 .entry(question.id.clone())
68 .or_default()
69 .push(provider.provider_id.clone());
70
71 first_question
73 .entry(question.id.clone())
74 .or_insert_with(|| question.clone());
75 }
76 }
77
78 let mut shared_questions = Vec::new();
85 let mut question_providers = HashMap::new();
86
87 const NEVER_SHARE_IDS: &[&str] = &[
89 "api_base_url", "bot_token", "access_token", "token", "app_id", "app_secret", "client_id", "client_secret", "webhook_secret", "signing_secret", ];
100
101 for (question_id, provider_ids) in &question_count {
102 let appears_multiple = provider_ids.len() >= 2;
103
104 if appears_multiple && let Some(question) = first_question.get(question_id) {
106 if question.secret {
108 continue;
109 }
110
111 if NEVER_SHARE_IDS.contains(&question_id.as_str()) {
113 continue;
114 }
115
116 shared_questions.push(question.clone());
117 question_providers.insert(question_id.clone(), provider_ids.clone());
118 }
119 }
120
121 shared_questions.sort_by(|a, b| a.id.cmp(&b.id));
123
124 SharedQuestionsResult {
125 shared_questions,
126 question_providers,
127 }
128}
129
130pub fn prompt_shared_questions(
135 shared: &SharedQuestionsResult,
136 advanced: bool,
137 existing_answers: &Value,
138) -> Result<Value> {
139 if shared.shared_questions.is_empty() {
140 return Ok(Value::Object(JsonMap::new()));
141 }
142
143 let existing_map = existing_answers.as_object();
144
145 let questions_needing_prompt: Vec<_> = shared
147 .shared_questions
148 .iter()
149 .filter(|q| {
150 if !advanced && !q.required {
152 return false;
153 }
154 if let Some(map) = existing_map
156 && let Some(value) = map.get(&q.id)
157 {
158 if !value.is_null() {
160 if let Some(s) = value.as_str() {
161 return s.is_empty(); }
163 return false; }
165 }
166 true })
168 .collect();
169
170 if questions_needing_prompt.is_empty() {
172 let mut answers = JsonMap::new();
173 if let Some(map) = existing_map {
174 for question in &shared.shared_questions {
175 if let Some(value) = map.get(&question.id) {
176 answers.insert(question.id.clone(), value.clone());
177 }
178 }
179 }
180 return Ok(Value::Object(answers));
181 }
182
183 println!("\n── Shared Configuration ──");
184 println!("The following settings apply to all providers:\n");
185
186 let mut answers = JsonMap::new();
187
188 if let Some(map) = existing_map {
190 for question in &shared.shared_questions {
191 if let Some(value) = map.get(&question.id)
192 && !value.is_null()
193 && !(value.is_string() && value.as_str() == Some(""))
194 {
195 answers.insert(question.id.clone(), value.clone());
196 }
197 }
198 }
199
200 for question in &shared.shared_questions {
201 if answers.contains_key(&question.id) {
203 continue;
204 }
205
206 if !advanced && !question.required {
208 continue;
209 }
210
211 if let Some(provider_ids) = shared.question_providers.get(&question.id) {
213 let providers_str = provider_ids
214 .iter()
215 .map(|id| setup_to_formspec::strip_domain_prefix(id))
216 .collect::<Vec<_>>()
217 .join(", ");
218 println!(" Used by: {providers_str}");
219 }
220
221 if let Some(value) = ask_form_spec_question(question)? {
222 answers.insert(question.id.clone(), value);
223 }
224 }
225
226 println!();
227 Ok(Value::Object(answers))
228}
229
230pub fn merge_shared_with_provider_answers(
235 shared: &Value,
236 provider_specific: Option<&Value>,
237) -> Value {
238 let mut merged = JsonMap::new();
239
240 if let Some(shared_map) = shared.as_object() {
242 for (key, value) in shared_map {
243 if !(value.is_null() || value.is_string() && value.as_str() == Some("")) {
245 merged.insert(key.clone(), value.clone());
246 }
247 }
248 }
249
250 if let Some(provider_map) = provider_specific.and_then(Value::as_object) {
252 for (key, value) in provider_map {
253 if !merged.contains_key(key) {
255 merged.insert(key.clone(), value.clone());
256 }
257 }
258 }
259
260 Value::Object(merged)
261}
262
263pub fn build_provider_form_specs(
267 providers: &[(std::path::PathBuf, String)], ) -> Vec<ProviderFormSpec> {
269 providers
270 .iter()
271 .filter_map(|(pack_path, provider_id)| {
272 setup_to_formspec::pack_to_form_spec(pack_path, provider_id).map(|form_spec| {
273 ProviderFormSpec {
274 provider_id: provider_id.clone(),
275 form_spec,
276 }
277 })
278 })
279 .collect()
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use qa_spec::QuestionType;
286
287 fn make_provider_form_spec(provider_id: &str, question_ids: &[&str]) -> ProviderFormSpec {
288 let questions = question_ids
289 .iter()
290 .map(|id| QuestionSpec {
291 id: id.to_string(),
292 kind: QuestionType::String,
293 title: format!("{} Question", id),
294 title_i18n: None,
295 description: None,
296 description_i18n: None,
297 required: true,
298 choices: None,
299 default_value: None,
300 secret: false,
301 visible_if: None,
302 constraint: None,
303 list: None,
304 computed: None,
305 policy: Default::default(),
306 computed_overridable: false,
307 })
308 .collect();
309
310 ProviderFormSpec {
311 provider_id: provider_id.to_string(),
312 form_spec: FormSpec {
313 id: format!("{}-setup", provider_id),
314 title: format!("{} Setup", provider_id),
315 version: "1.0.0".into(),
316 description: None,
317 presentation: None,
318 progress_policy: None,
319 secrets_policy: None,
320 store: vec![],
321 validations: vec![],
322 includes: vec![],
323 questions,
324 },
325 }
326 }
327
328 #[test]
329 fn collect_shared_questions_finds_common_questions() {
330 let providers = vec![
331 make_provider_form_spec("messaging-telegram", &["public_base_url", "bot_token"]),
332 make_provider_form_spec("messaging-slack", &["public_base_url", "slack_token"]),
333 make_provider_form_spec("messaging-teams", &["public_base_url", "teams_app_id"]),
334 ];
335
336 let result = collect_shared_questions(&providers);
337
338 assert_eq!(result.shared_questions.len(), 1);
340 assert_eq!(result.shared_questions[0].id, "public_base_url");
341
342 let providers_for_url = result.question_providers.get("public_base_url").unwrap();
344 assert_eq!(providers_for_url.len(), 3);
345 assert!(providers_for_url.contains(&"messaging-telegram".to_string()));
346 assert!(providers_for_url.contains(&"messaging-slack".to_string()));
347 assert!(providers_for_url.contains(&"messaging-teams".to_string()));
348 }
349
350 #[test]
351 fn collect_shared_questions_excludes_single_provider_questions() {
352 let providers = vec![
353 make_provider_form_spec("messaging-telegram", &["public_base_url", "bot_token"]),
354 make_provider_form_spec("messaging-slack", &["slack_token"]), ];
356
357 let result = collect_shared_questions(&providers);
358 assert!(result.shared_questions.is_empty());
359 }
360
361 #[test]
362 fn collect_shared_questions_returns_empty_for_single_provider() {
363 let providers = vec![make_provider_form_spec(
364 "messaging-telegram",
365 &["public_base_url", "bot_token"],
366 )];
367
368 let result = collect_shared_questions(&providers);
369 assert!(result.shared_questions.is_empty());
370 }
371
372 #[test]
373 fn collect_shared_questions_finds_non_wellknown_duplicates() {
374 let providers = vec![
375 make_provider_form_spec("provider-a", &["custom_field", "field_a"]),
376 make_provider_form_spec("provider-b", &["custom_field", "field_b"]),
377 ];
378
379 let result = collect_shared_questions(&providers);
380 assert_eq!(result.shared_questions.len(), 1);
381 assert_eq!(result.shared_questions[0].id, "custom_field");
382 }
383
384 #[test]
385 fn collect_shared_questions_deduplicates() {
386 let providers = vec![
387 make_provider_form_spec("provider-a", &["public_base_url"]),
388 make_provider_form_spec("provider-b", &["public_base_url"]),
389 make_provider_form_spec("provider-c", &["public_base_url"]),
390 ];
391
392 let result = collect_shared_questions(&providers);
393 assert_eq!(result.shared_questions.len(), 1);
394 }
395}