Skip to main content

greentic_bundle/setup/
persist.rs

1use 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}