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}