use std::sync::Arc;
use rainier_support::Result;
use crate::model::Model;
use crate::repository::Repository;
type State<M> = Arc<dyn Fn(&mut M, usize) + Send + Sync>;
pub struct Factory<M> {
make: Arc<dyn Fn(usize) -> M + Send + Sync>,
states: Vec<State<M>>,
count: usize,
}
impl<M> Factory<M> {
pub fn new(make: impl Fn(usize) -> M + Send + Sync + 'static) -> Self {
Self { make: Arc::new(make), states: Vec::new(), count: 1 }
}
#[must_use = "this returns a configured factory rather than configuring in place"]
pub fn count(mut self, count: usize) -> Self {
self.count = count;
self
}
#[must_use = "this returns a configured factory rather than configuring in place"]
pub fn state(self, state: impl Fn(&mut M) + Send + Sync + 'static) -> Self {
self.sequence(move |model, _| state(model))
}
#[must_use = "this returns a configured factory rather than configuring in place"]
pub fn sequence(mut self, state: impl Fn(&mut M, usize) + Send + Sync + 'static) -> Self {
self.states.push(Arc::new(state));
self
}
pub fn make(&self) -> Vec<M> {
(0..self.count)
.map(|index| {
let mut model = (self.make)(index);
for state in &self.states {
state(&mut model, index);
}
model
})
.collect()
}
pub fn make_one(&self) -> M {
let mut model = (self.make)(0);
for state in &self.states {
state(&mut model, 0);
}
model
}
}
impl<M: Model> Factory<M> {
pub async fn create(&self, repository: &dyn Repository<M>) -> Result<Vec<M>> {
let mut created = Vec::with_capacity(self.count);
for model in self.make() {
created.push(repository.create(model).await?);
}
Ok(created)
}
pub async fn create_one(&self, repository: &dyn Repository<M>) -> Result<M> {
repository.create(self.make_one()).await
}
}
impl<M> Clone for Factory<M> {
fn clone(&self) -> Self {
Self { make: Arc::clone(&self.make), states: self.states.clone(), count: self.count }
}
}
impl<M> std::fmt::Debug for Factory<M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Factory")
.field("count", &self.count)
.field("states", &self.states.len())
.finish()
}
}
pub trait HasFactory: Sized {
fn factory() -> Factory<Self>;
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, Default, PartialEq)]
struct User {
id: u64,
email: String,
admin: bool,
}
impl HasFactory for User {
fn factory() -> Factory<Self> {
Factory::new(|_| User::default())
}
}
#[test]
fn one_by_default() {
let users = User::factory().make();
assert_eq!(users.len(), 1);
assert_eq!(users[0], User::default());
}
#[test]
fn count_builds_that_many() {
assert_eq!(User::factory().count(3).make().len(), 3);
assert_eq!(User::factory().count(0).make().len(), 0);
}
#[test]
fn state_applies_to_every_one() {
let users = User::factory().count(3).state(|user| user.admin = true).make();
assert!(users.iter().all(|user| user.admin));
}
#[test]
fn a_sequence_knows_which_row_it_is() {
let users = User::factory()
.count(3)
.sequence(|user, i| user.email = format!("user{i}@example.com"))
.make();
assert_eq!(users[0].email, "user0@example.com");
assert_eq!(users[2].email, "user2@example.com");
let emails: std::collections::HashSet<_> = users.iter().map(|user| &user.email).collect();
assert_eq!(emails.len(), 3, "a unique column needs distinct values");
}
#[test]
fn states_apply_in_the_order_they_were_added() {
let users = User::factory()
.state(|user| user.email = "first".into())
.state(|user| user.email = "second".into())
.make();
assert_eq!(users[0].email, "second");
}
#[test]
fn make_one_ignores_the_count() {
let user = User::factory().count(5).state(|user| user.admin = true).make_one();
assert!(user.admin);
}
#[test]
fn a_factory_can_be_cloned_and_specialised() {
let base = User::factory().count(2);
let admins = base.clone().state(|user| user.admin = true).make();
let ordinary = base.make();
assert!(admins.iter().all(|user| user.admin));
assert!(!ordinary.iter().any(|user| user.admin));
}
#[test]
fn a_hand_written_factory_can_do_what_a_derived_one_cannot() {
struct Post {
slug: String,
}
impl HasFactory for Post {
fn factory() -> Factory<Self> {
Factory::new(|i| Post { slug: format!("post-{i}") })
}
}
let posts = Post::factory().count(2).make();
assert_eq!(posts[0].slug, "post-0");
assert_eq!(posts[1].slug, "post-1");
}
}