greentic_bundle/setup/
persist.rs1use std::collections::BTreeMap;
2use std::path::Path;
3
4use anyhow::{Result, bail};
5use serde_json::Value;
6
7use super::backend::SetupBackend;
8use super::legacy_formspec::normalize_answer_value;
9use super::{
10 FormSpec, PersistedSetupState, QuestionSpec, SETUP_STATE_DIR, SETUP_STATE_SCHEMA_VERSION,
11 SetupSpecInput,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct SetupInstruction {
16 pub provider_id: String,
17 pub spec_input: SetupSpecInput,
18 pub answers: Value,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct SetupPersistenceResult {
23 pub states: Vec<PersistedSetupState>,
24 pub writes: Vec<String>,
25}
26
27pub fn collect_setup_instructions(
28 specs: &BTreeMap<String, Value>,
29 answers: &BTreeMap<String, Value>,
30) -> Result<Vec<SetupInstruction>> {
31 let mut provider_ids = specs.keys().cloned().collect::<Vec<_>>();
32 provider_ids.sort();
33 provider_ids.dedup();
34
35 let mut instructions = Vec::new();
36 for provider_id in provider_ids {
37 let spec_input = parse_spec_input(
38 specs
39 .get(&provider_id)
40 .cloned()
41 .ok_or_else(|| anyhow::anyhow!("missing setup spec for {provider_id}"))?,
42 )?;
43 let provider_answers = answers
44 .get(&provider_id)
45 .cloned()
46 .unwrap_or_else(|| Value::Object(Default::default()));
47 instructions.push(SetupInstruction {
48 provider_id,
49 spec_input,
50 answers: provider_answers,
51 });
52 }
53 Ok(instructions)
54}
55
56pub fn persist_setup(
57 root: &Path,
58 instructions: &[SetupInstruction],
59 backend: &dyn SetupBackend,
60) -> Result<SetupPersistenceResult> {
61 let mut states = Vec::new();
62 let mut writes = Vec::new();
63 for instruction in instructions {
64 let state = build_state(instruction)?;
65 if let Some(path) = backend.persist(&state)? {
66 writes.push(relative_display(root, &path));
67 } else {
68 writes.push(format!(
69 "{SETUP_STATE_DIR}/{}.json",
70 instruction.provider_id
71 ));
72 }
73 states.push(state);
74 }
75 writes.sort();
76 writes.dedup();
77 Ok(SetupPersistenceResult { states, writes })
78}
79
80fn build_state(instruction: &SetupInstruction) -> Result<PersistedSetupState> {
81 let (source_kind, form) =
82 super::form_spec_from_input(&instruction.spec_input, &instruction.provider_id)?;
83
84 let normalized_answers = normalize_answers(&form, &instruction.answers)?;
85 let mut non_secret_config = BTreeMap::new();
86 let mut secret_values = BTreeMap::new();
87 for question in &form.questions {
88 if let Some(value) = normalized_answers.get(&question.id) {
89 if question.secret {
90 secret_values.insert(question.id.clone(), value.clone());
91 } else {
92 non_secret_config.insert(question.id.clone(), value.clone());
93 }
94 }
95 }
96
97 Ok(PersistedSetupState {
98 schema_version: SETUP_STATE_SCHEMA_VERSION,
99 provider_id: instruction.provider_id.clone(),
100 source_kind,
101 form,
102 normalized_answers,
103 non_secret_config,
104 secret_values,
105 })
106}
107
108fn normalize_answers(form: &FormSpec, answers: &Value) -> Result<BTreeMap<String, Value>> {
109 let map = answers
110 .as_object()
111 .ok_or_else(|| anyhow::anyhow!("setup answers must be a JSON object"))?;
112 let mut normalized = BTreeMap::new();
113 for question in &form.questions {
114 match map.get(&question.id) {
115 Some(value) => {
116 normalized.insert(
117 question.id.clone(),
118 normalize_question_answer(question, value)?,
119 );
120 }
121 None if question.required => bail!("missing required setup answer for {}", question.id),
122 None => {
123 if let Some(default) = &question.default_value {
124 normalized.insert(question.id.clone(), default.clone());
125 }
126 }
127 }
128 }
129 Ok(normalized)
130}
131
132fn normalize_question_answer(question: &QuestionSpec, value: &Value) -> Result<Value> {
133 normalize_answer_value(question, value)
134}
135
136fn parse_spec_input(value: Value) -> Result<SetupSpecInput> {
137 serde_json::from_value(value).map_err(Into::into)
138}
139
140fn relative_display(root: &Path, path: &Path) -> String {
141 path.strip_prefix(root)
142 .unwrap_or(path)
143 .display()
144 .to_string()
145}