use super::Factory;
use crate::error::{OrmError, OrmResult};
use crate::model::Model;
#[derive(Debug, Clone, PartialEq)]
pub enum Environment {
Development,
Testing,
Staging,
Production,
Custom(String),
}
impl Environment {
pub fn from_str(env: &str) -> Self {
match env.to_lowercase().as_str() {
"development" | "dev" => Environment::Development,
"testing" | "test" => Environment::Testing,
"staging" | "stage" => Environment::Staging,
"production" | "prod" => Environment::Production,
custom => Environment::Custom(custom.to_string()),
}
}
pub fn as_str(&self) -> &str {
match self {
Environment::Development => "development",
Environment::Testing => "testing",
Environment::Staging => "staging",
Environment::Production => "production",
Environment::Custom(name) => name,
}
}
pub fn is_safe_for_seeding(&self) -> bool {
match self {
Environment::Development | Environment::Testing => true,
Environment::Staging => true, Environment::Production => false, Environment::Custom(_) => false, }
}
}
#[async_trait::async_trait]
pub trait Seeder: Send + Sync {
fn name(&self) -> &str;
fn environments(&self) -> Vec<Environment> {
vec![Environment::Development, Environment::Testing]
}
fn should_run(&self, env: &Environment) -> bool {
self.environments().contains(env)
}
async fn run(&self, pool: &sqlx::Pool<sqlx::Postgres>) -> OrmResult<()>;
async fn rollback(&self, _pool: &sqlx::Pool<sqlx::Postgres>) -> OrmResult<()> {
Ok(())
}
fn priority(&self) -> i32 {
100
}
fn dependencies(&self) -> Vec<String> {
vec![]
}
}
pub struct FactorySeeder<T: Model, F: Factory<T>> {
name: String,
factory: F,
count: usize,
environments: Vec<Environment>,
priority: i32,
dependencies: Vec<String>,
_phantom: std::marker::PhantomData<T>,
}
impl<T: Model, F: Factory<T>> FactorySeeder<T, F> {
pub fn new(name: impl Into<String>, factory: F, count: usize) -> Self {
Self {
name: name.into(),
factory,
count,
environments: vec![Environment::Development, Environment::Testing],
priority: 100,
dependencies: vec![],
_phantom: std::marker::PhantomData,
}
}
pub fn environments(mut self, envs: Vec<Environment>) -> Self {
self.environments = envs;
self
}
pub fn with_priority(mut self, priority: i32) -> Self {
self.priority = priority;
self
}
pub fn depends_on(mut self, dependencies: Vec<String>) -> Self {
self.dependencies = dependencies;
self
}
}
#[async_trait::async_trait]
impl<T: Model + Send + Sync, F: Factory<T> + Send + Sync> Seeder for FactorySeeder<T, F> {
fn name(&self) -> &str {
&self.name
}
fn environments(&self) -> Vec<Environment> {
self.environments.clone()
}
fn priority(&self) -> i32 {
self.priority
}
fn dependencies(&self) -> Vec<String> {
self.dependencies.clone()
}
async fn run(&self, pool: &sqlx::Pool<sqlx::Postgres>) -> OrmResult<()> {
tracing::info!(
"Running seeder: {} (creating {} records)",
self.name,
self.count
);
let models = self.factory.create_many(pool, self.count).await?;
tracing::info!(
"Seeder {} completed: created {} records",
self.name,
models.len()
);
Ok(())
}
async fn rollback(&self, _pool: &sqlx::Pool<sqlx::Postgres>) -> OrmResult<()> {
tracing::info!("Rolling back seeder: {}", self.name);
tracing::warn!("Rollback not yet implemented for factory seeders");
Ok(())
}
}
#[derive(Default)]
pub struct SeederManager {
seeders: Vec<Box<dyn Seeder>>,
}
impl SeederManager {
pub fn new() -> Self {
Self::default()
}
pub fn add<S: Seeder + 'static>(mut self, seeder: S) -> Self {
self.seeders.push(Box::new(seeder));
self
}
pub fn add_factory<T, F>(self, name: impl Into<String>, factory: F, count: usize) -> Self
where
T: Model + Send + Sync + 'static,
F: Factory<T> + Send + Sync + 'static,
{
let seeder = FactorySeeder::new(name, factory, count);
self.add(seeder)
}
pub async fn run_for_environment(
&self,
pool: &sqlx::Pool<sqlx::Postgres>,
env: &Environment,
) -> OrmResult<()> {
let mut applicable_seeders: Vec<&Box<dyn Seeder>> = self
.seeders
.iter()
.filter(|seeder| seeder.should_run(env))
.collect();
applicable_seeders.sort_by_key(|seeder| seeder.priority());
tracing::info!(
"Running {} seeders for environment: {}",
applicable_seeders.len(),
env.as_str()
);
if !env.is_safe_for_seeding() {
return Err(OrmError::Validation(format!(
"Environment '{}' is not safe for automatic seeding. Use explicit opt-in.",
env.as_str()
)));
}
let ordered_seeders = self.resolve_dependencies(applicable_seeders)?;
for seeder in ordered_seeders {
tracing::info!("Running seeder: {}", seeder.name());
seeder.run(pool).await?;
}
tracing::info!("All seeders completed successfully");
Ok(())
}
pub async fn run_development(&self, pool: &sqlx::Pool<sqlx::Postgres>) -> OrmResult<()> {
self.run_for_environment(pool, &Environment::Development)
.await
}
pub async fn run_testing(&self, pool: &sqlx::Pool<sqlx::Postgres>) -> OrmResult<()> {
self.run_for_environment(pool, &Environment::Testing).await
}
pub async fn run_production_force(&self, pool: &sqlx::Pool<sqlx::Postgres>) -> OrmResult<()> {
let applicable_seeders: Vec<&Box<dyn Seeder>> = self
.seeders
.iter()
.filter(|seeder| seeder.environments().contains(&Environment::Production))
.collect();
let ordered_seeders = self.resolve_dependencies(applicable_seeders)?;
tracing::warn!(
"Force running {} seeders in PRODUCTION environment with dependency resolution",
ordered_seeders.len()
);
for seeder in ordered_seeders {
tracing::warn!("Running production seeder: {}", seeder.name());
seeder.run(pool).await?;
}
Ok(())
}
fn resolve_dependencies<'a>(
&self,
seeders: Vec<&'a Box<dyn Seeder>>,
) -> OrmResult<Vec<&'a Box<dyn Seeder>>> {
use std::collections::{HashMap, HashSet, VecDeque};
let mut seeder_map: HashMap<String, &'a Box<dyn Seeder>> = HashMap::new();
let mut dependencies: HashMap<String, Vec<String>> = HashMap::new();
let mut in_degree: HashMap<String, usize> = HashMap::new();
for seeder in &seeders {
let name = seeder.name().to_string();
seeder_map.insert(name.clone(), seeder);
dependencies.insert(name.clone(), seeder.dependencies());
in_degree.insert(name.clone(), 0);
}
let mut rev_deps: HashMap<String, Vec<String>> = HashMap::new();
for (seeder_name, deps) in &dependencies {
for dep in deps {
if !seeder_map.contains_key(dep) {
return Err(OrmError::Validation(format!(
"Seeder '{}' depends on '{}', but '{}' was not found",
seeder_name, dep, dep
)));
}
rev_deps.entry(dep.clone()).or_default().push(seeder_name.clone());
if let Some(degree) = in_degree.get_mut(seeder_name) {
*degree += 1;
}
}
}
let mut queue: VecDeque<String> = VecDeque::new();
let mut result = Vec::new();
let mut processed = HashSet::new();
for (name, °ree) in &in_degree {
if degree == 0 {
queue.push_back(name.clone());
}
}
while let Some(current) = queue.pop_front() {
processed.insert(current.clone());
if let Some(seeder) = seeder_map.get(¤t) {
result.push(*seeder);
}
if let Some(dependents) = rev_deps.get(¤t) {
for dependent in dependents {
if let Some(degree) = in_degree.get_mut(dependent) {
*degree -= 1;
if *degree == 0 {
queue.push_back(dependent.clone());
}
}
}
}
}
if result.len() != seeders.len() {
let unprocessed: Vec<String> = seeders
.iter()
.map(|s| s.name().to_string())
.filter(|name| !processed.contains(name))
.collect();
return Err(OrmError::Validation(format!(
"Circular dependency detected in seeders: {}",
unprocessed.join(", ")
)));
}
result.sort_by_key(|seeder| seeder.priority());
Ok(result)
}
pub fn current_environment() -> Environment {
std::env::var("ELIF_ENV")
.or_else(|_| std::env::var("ENV"))
.or_else(|_| std::env::var("ENVIRONMENT"))
.map(|env| Environment::from_str(&env))
.unwrap_or(Environment::Development)
}
pub async fn run(&self, pool: &sqlx::Pool<sqlx::Postgres>) -> OrmResult<()> {
let env = Self::current_environment();
self.run_for_environment(pool, &env).await
}
}
pub struct CustomSeeder {
name: String,
environments: Vec<Environment>,
priority: i32,
dependencies: Vec<String>,
run_fn: Box<
dyn Fn(
&sqlx::Pool<sqlx::Postgres>,
)
-> std::pin::Pin<Box<dyn std::future::Future<Output = OrmResult<()>> + Send>>
+ Send
+ Sync,
>,
}
impl CustomSeeder {
pub fn new<F, Fut>(name: impl Into<String>, run_fn: F) -> Self
where
F: Fn(&sqlx::Pool<sqlx::Postgres>) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = OrmResult<()>> + Send + 'static,
{
Self {
name: name.into(),
environments: vec![Environment::Development, Environment::Testing],
priority: 100,
dependencies: vec![],
run_fn: Box::new(move |pool| Box::pin(run_fn(pool))),
}
}
pub fn environments(mut self, envs: Vec<Environment>) -> Self {
self.environments = envs;
self
}
pub fn with_priority(mut self, priority: i32) -> Self {
self.priority = priority;
self
}
pub fn depends_on(mut self, dependencies: Vec<String>) -> Self {
self.dependencies = dependencies;
self
}
}
#[async_trait::async_trait]
impl Seeder for CustomSeeder {
fn name(&self) -> &str {
&self.name
}
fn environments(&self) -> Vec<Environment> {
self.environments.clone()
}
fn priority(&self) -> i32 {
self.priority
}
fn dependencies(&self) -> Vec<String> {
self.dependencies.clone()
}
async fn run(&self, pool: &sqlx::Pool<sqlx::Postgres>) -> OrmResult<()> {
(self.run_fn)(pool).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_environment_parsing() {
assert_eq!(
Environment::from_str("development"),
Environment::Development
);
assert_eq!(Environment::from_str("dev"), Environment::Development);
assert_eq!(Environment::from_str("testing"), Environment::Testing);
assert_eq!(Environment::from_str("test"), Environment::Testing);
assert_eq!(Environment::from_str("production"), Environment::Production);
assert_eq!(Environment::from_str("prod"), Environment::Production);
assert_eq!(
Environment::from_str("custom"),
Environment::Custom("custom".to_string())
);
}
#[test]
fn test_environment_safety() {
assert!(Environment::Development.is_safe_for_seeding());
assert!(Environment::Testing.is_safe_for_seeding());
assert!(Environment::Staging.is_safe_for_seeding());
assert!(!Environment::Production.is_safe_for_seeding());
assert!(!Environment::Custom("custom".to_string()).is_safe_for_seeding());
}
#[test]
fn test_seeder_manager_creation() {
let manager = SeederManager::new();
assert_eq!(manager.seeders.len(), 0);
}
#[test]
fn test_current_environment() {
let env = SeederManager::current_environment();
assert_eq!(env, Environment::Development);
}
#[tokio::test]
async fn test_custom_seeder() {
let seeder = CustomSeeder::new("test_seeder", |_pool| async { Ok(()) });
assert_eq!(seeder.name(), "test_seeder");
assert_eq!(Seeder::priority(&seeder), 100);
assert!(seeder.should_run(&Environment::Development));
}
}