rws_server/model/seed.rs
1//! Seeds written in Rust (18.30.0), run after the `seeds/*.sql` files by
2//! [`DbPool::seed_with`] — and by an app's own `db seed` command, built on
3//! `rust_web_server::db_cli::run_with_seeders`.
4//!
5//! A seeder is a name and an async function of the pool; factories
6//! (`#[derive(Factory)]`) make the rows:
7//!
8//! ```rust,ignore
9//! use rust_web_server::model::{DbPool, Seeders};
10//!
11//! pub fn seeders() -> Seeders {
12//! Seeders::new()
13//! .add("users", |pool: DbPool| async move {
14//! User::factory().create_many(10, &pool).await?;
15//! Ok(())
16//! })
17//! .add("admin", |pool: DbPool| async move {
18//! User::factory().with(|u| u.admin = true).create(&pool).await?;
19//! Ok(())
20//! })
21//! }
22//!
23//! pool.seed_with("seeds", &seeders()).await?; // seeds/*.sql, then users, admin
24//! ```
25//!
26//! Like SQL seeds, a seeder runs every time it is asked to — nothing
27//! records that it ran — so write it to tolerate the rows being there
28//! (look them up first, or use keys that a second insert can't repeat).
29//! It isn't wrapped in a transaction: use [`DbPool::transaction`] in it
30//! when its rows must go in together.
31
32use std::future::Future;
33use std::pin::Pin;
34use std::sync::Arc;
35
36use super::pool::DbPool;
37use super::DbError;
38
39/// The future a [`Seeder`] answers.
40pub type SeedFuture<'a> = Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>>;
41
42/// Rows put in a database by Rust code; see the [module docs](self).
43/// [`Seeders::add`] makes one from a closure.
44pub trait Seeder: Send + Sync {
45 /// What `seed_with` and `db seed` report it as.
46 fn name(&self) -> &str;
47
48 /// Put the rows in `pool` (the primary of a replicated pool).
49 fn run<'a>(&'a self, pool: &'a DbPool) -> SeedFuture<'a>;
50}
51
52struct FnSeeder<F> {
53 name: String,
54 run: F,
55}
56
57impl<F, Fut> Seeder for FnSeeder<F>
58where
59 F: Fn(DbPool) -> Fut + Send + Sync,
60 Fut: Future<Output = Result<(), DbError>> + Send + 'static,
61{
62 fn name(&self) -> &str {
63 &self.name
64 }
65
66 fn run<'a>(&'a self, pool: &'a DbPool) -> SeedFuture<'a> {
67 Box::pin((self.run)(pool.clone()))
68 }
69}
70
71/// Seeders, run in the order added. Cheap to clone.
72#[derive(Clone, Default)]
73pub struct Seeders {
74 list: Vec<Arc<dyn Seeder>>,
75}
76
77impl std::fmt::Debug for Seeders {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.debug_list().entries(self.names()).finish()
80 }
81}
82
83impl Seeders {
84 /// No seeders.
85 pub fn new() -> Self {
86 Seeders::default()
87 }
88
89 /// Add a seeder named `name` that runs `run` with the pool.
90 pub fn add<F, Fut>(self, name: impl Into<String>, run: F) -> Self
91 where
92 F: Fn(DbPool) -> Fut + Send + Sync + 'static,
93 Fut: Future<Output = Result<(), DbError>> + Send + 'static,
94 {
95 self.seeder(FnSeeder { name: name.into(), run })
96 }
97
98 /// Add a [`Seeder`] implemented by hand.
99 pub fn seeder(mut self, seeder: impl Seeder + 'static) -> Self {
100 self.list.push(Arc::new(seeder));
101 self
102 }
103
104 /// The seeders' names, in order.
105 pub fn names(&self) -> Vec<&str> {
106 self.list.iter().map(|s| s.name()).collect()
107 }
108
109 /// How many there are.
110 pub fn len(&self) -> usize {
111 self.list.len()
112 }
113
114 /// Whether there are none.
115 pub fn is_empty(&self) -> bool {
116 self.list.is_empty()
117 }
118
119 /// Run every seeder on `pool`'s primary, in order; answers their names.
120 /// Stops at the first that fails, with its name in the error (what it
121 /// wrote before failing stays).
122 pub async fn run(&self, pool: &DbPool) -> Result<Vec<String>, DbError> {
123 let primary = pool.primary();
124 let mut ran = Vec::with_capacity(self.list.len());
125 for seeder in &self.list {
126 seeder.run(primary).await.map_err(|e| DbError::new(format!("seeder '{}' failed: {}", seeder.name(), e.0)))?;
127 ran.push(seeder.name().to_string());
128 }
129 Ok(ran)
130 }
131}
132
133impl DbPool {
134 /// [`seed`](DbPool::seed) the `*.sql` files in `dir`, then run
135 /// `seeders` (18.30.0); answers the files and the seeders' names, in the
136 /// order they ran. With seeders, a missing `dir` means no SQL seeds
137 /// rather than an error.
138 pub async fn seed_with(&self, dir: &str, seeders: &Seeders) -> Result<Vec<String>, DbError> {
139 let mut ran = if seeders.is_empty() || std::path::Path::new(dir).is_dir() { self.seed(dir).await? } else { Vec::new() };
140 ran.extend(seeders.run(self).await?);
141 Ok(ran)
142 }
143}