Skip to main content

database_bootstrap/
step.rs

1//! Bootstrap step trait and related types.
2
3use async_trait::async_trait;
4use std::collections::HashMap;
5use std::fmt::Display;
6use uuid::Uuid;
7
8use crate::BootstrapError;
9use crate::traits::DatabaseConnection;
10
11/// Result of verifying whether a bootstrap step's data exists.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum VerifyResult {
14    /// The data is present and correct.
15    Present,
16    /// The data is missing entirely.
17    Missing,
18    /// The data is partially present (inconsistent state).
19    Partial,
20}
21
22/// Detailed verification result with entity-level information.
23///
24/// This type allows steps to pass verification data to run() to avoid
25/// redundant database queries.
26#[derive(Debug, Clone)]
27pub struct VerifyDetails {
28    /// Overall verification result.
29    pub result: VerifyResult,
30    /// Entity existence status (entity_name -> exists).
31    pub entities: HashMap<&'static str, bool>,
32    /// Entity IDs discovered during verification (entity_name -> id).
33    pub ids: HashMap<&'static str, Uuid>,
34}
35
36impl VerifyDetails {
37    /// Creates a new details instance with the given result.
38    pub fn new(result: VerifyResult) -> Self {
39        Self {
40            result,
41            entities: HashMap::new(),
42            ids: HashMap::new(),
43        }
44    }
45
46    /// Records an entity's existence status.
47    pub fn entity(mut self, name: &'static str, exists: bool) -> Self {
48        self.entities.insert(name, exists);
49        self
50    }
51
52    /// Records an entity's existence status with its ID.
53    pub fn entity_with_id(mut self, name: &'static str, exists: bool, id: Uuid) -> Self {
54        self.entities.insert(name, exists);
55        if exists {
56            self.ids.insert(name, id);
57        }
58        self
59    }
60
61    /// Returns true if all entities exist.
62    pub fn all_present(&self) -> bool {
63        self.entities.values().all(|&v| v)
64    }
65
66    /// Returns true if any entity exists.
67    pub fn any_present(&self) -> bool {
68        self.entities.values().any(|&v| v)
69    }
70
71    /// Returns true if no entities exist.
72    pub fn none_present(&self) -> bool {
73        !self.any_present()
74    }
75
76    /// Checks if a specific entity exists.
77    pub fn has(&self, name: &str) -> bool {
78        self.entities.get(name).copied().unwrap_or(false)
79    }
80
81    /// Gets the ID for an entity, if it exists.
82    pub fn id(&self, name: &str) -> Option<Uuid> {
83        self.ids.get(name).copied()
84    }
85}
86
87/// Requirement level for bootstrap step data.
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum Requirement {
90    /// Data must be present after bootstrap.
91    /// Verification will fail if the step completes but data is still missing.
92    Required,
93    /// Data is optional.
94    /// Verification will pass even if the step cannot create the data.
95    Optional,
96}
97
98/// Output from a bootstrap step that should be displayed in the summary.
99///
100/// Used for secrets like passwords and tokens that are generated during bootstrap
101/// and need to be shown to the operator exactly once.
102#[derive(Debug, Clone)]
103pub struct StepOutput {
104    /// Human-readable title for this output.
105    pub title: String,
106    /// Key-value pairs to display.
107    pub fields: Vec<(String, String)>,
108    /// Whether this output contains sensitive data (passwords, tokens, etc.).
109    pub is_sensitive: bool,
110}
111
112impl StepOutput {
113    /// Creates a new output with a title.
114    pub fn new(title: impl Into<String>) -> Self {
115        Self {
116            title: title.into(),
117            fields: Vec::new(),
118            is_sensitive: false,
119        }
120    }
121
122    /// Adds a field to the output.
123    pub fn field(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
124        self.fields.push((key.into(), value.into()));
125        self
126    }
127
128    /// Marks the output as containing sensitive data.
129    pub fn sensitive(mut self) -> Self {
130        self.is_sensitive = true;
131        self
132    }
133}
134
135impl Display for StepOutput {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        writeln!(f, "  {}", self.title)?;
138        for (key, value) in &self.fields {
139            writeln!(f, "    {key}: {value}")?;
140        }
141        Ok(())
142    }
143}
144
145/// Factory for creating bootstrap steps.
146///
147/// Used for type-safe dependency declarations. When a step declares a dependency,
148/// it provides a factory that can create the dependency step if it's not already
149/// registered in the runner.
150pub trait StepFactory<T: DatabaseConnection>: Send + Sync {
151    /// Returns the name of the step this factory creates.
152    fn step_name(&self) -> &'static str;
153
154    /// Creates a new instance of the step.
155    fn create(&self) -> Box<dyn BootstrapStep<T>>;
156}
157
158/// A single bootstrap step that can verify and create seed data.
159///
160/// Steps can depend on other steps, forming a DAG. The runner executes
161/// steps in topological order after verifying dependencies are satisfied.
162///
163/// # Example
164///
165/// ```ignore
166/// struct MyStep;
167///
168/// #[async_trait]
169/// impl BootstrapStep for MyStep {
170///     fn name(&self) -> &'static str { "my_step" }
171///     fn description(&self) -> &'static str { "Does something" }
172///     
173///     async fn verify(&self, txn: &DatabaseTransaction) -> Result<VerifyDetails, BootstrapError> {
174///         // Check if data exists
175///         Ok(VerifyDetails::new(VerifyResult::Missing))
176///     }
177///     
178///     async fn run(&self, txn: &DatabaseTransaction, details: &VerifyDetails) -> Result<Option<StepOutput>, BootstrapError> {
179///         // Create data
180///         Ok(Some(StepOutput::new("My Step Output")
181///             .field("key", "value")
182///             .sensitive()))
183///     }
184/// }
185/// ```
186#[async_trait]
187pub trait BootstrapStep<T: DatabaseConnection>: Send + Sync {
188    /// Unique identifier for this step.
189    fn name(&self) -> &'static str;
190
191    /// Human-readable description of what this step does.
192    fn description(&self) -> &'static str;
193
194    /// Factories for steps that must run before this one.
195    ///
196    /// If a dependency is not registered in the runner, it will be
197    /// automatically created using the factory and executed first.
198    fn dependencies(&self) -> Vec<Box<dyn StepFactory<T>>> {
199        Vec::new()
200    }
201
202    /// Whether this step's data is required or optional.
203    fn requirement(&self) -> Requirement {
204        Requirement::Required
205    }
206
207    /// Whether to skip this step with a warning if data is missing.
208    ///
209    /// When `true` and verify returns `Missing`:
210    /// - Logs a warning
211    /// - Marks step as completed without running
212    /// - Does not auto-create the data
213    ///
214    /// When `false` (default) and verify returns `Missing`:
215    /// - Runs the step to create the data
216    fn skip_if_missing(&self) -> bool {
217        false
218    }
219
220    /// Check if the data for this step already exists.
221    ///
222    /// Returns detailed information about each entity's existence status.
223    /// This information is passed to run() to avoid redundant queries.
224    async fn verify(&self, txn: &T::DatabaseTransaction) -> Result<VerifyDetails, BootstrapError<T::Error>>;
225
226    /// Create the data for this step. Called only if verify returns Missing or Partial.
227    ///
228    /// Receives the verification details to avoid redundant queries.
229    /// Returns `Some(StepOutput)` if there are secrets or other information
230    /// that should be displayed in the bootstrap summary.
231    async fn run(
232        &self,
233        txn: &T::DatabaseTransaction,
234        details: &VerifyDetails,
235    ) -> Result<Option<StepOutput>, BootstrapError<T::Error>>;
236}