Skip to main content

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}