Add multi-environment support for database configuration

Introduces separate databases and config files for dev, test, and prod
environments. The application now defaults to production, with --env flag
to specify alternative environments.

Changes:
- Update config.rs to support env-based loading (config.toml -> config.<env>.toml -> config.example.toml)
- Add Env enum (Prod, Dev, Test) with database name mapping
- Add --env flag to CLI commands (defaults to prod)
- Add 'db setup' command to create database and schema
- Split migrations into env-specific database creation and shared schema
- Update .gitignore to track config.example.toml but ignore config.toml and config.<env>.toml files
- Update config.example.toml as a template with placeholder values
- Delete 001_initial_schema.sql, replaced by 002_schema.sql + env-specific files

Config loading order:
  1. config.toml (local override)
  2. config.<env>.toml (environment-specific)
  3. config.example.toml (fallback)

Database names:
  - prod: rusty_petroleum
  - dev:  rusty_petroleum_dev
  - test: rusty_petroleum_test

Usage:
  cargo run -- db setup --env dev       # Setup dev database
  cargo run -- import data.csv --env dev # Import to dev
  cargo run -- db setup                # Setup prod (default)
  cargo run -- import data.csv         # Import to prod (default)
This commit is contained in:
2026-04-02 07:09:06 +02:00
parent 9daa186ff6
commit cd46368f79
11 changed files with 279 additions and 38 deletions
+85 -22
View File
@@ -5,7 +5,7 @@ mod invoice_generator;
use askama::Template;
use chrono::{NaiveDateTime, Utc};
use config::Config;
use config::{Config, Env};
use csv::ReaderBuilder;
use db::{create_pool, Repository};
use invoice_generator::{group_by_customer, read_csv_file, Customer};
@@ -233,10 +233,41 @@ struct CustomerTemplate {
generated_date: String,
}
fn parse_env_flag(args: &[String]) -> (Env, usize) {
for (i, arg) in args.iter().enumerate() {
if arg == "--env" && i + 1 < args.len() {
match args[i + 1].parse() {
Ok(env) => return (env, i),
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
}
}
(Env::default(), 0)
}
fn remove_env_flags(args: &[String]) -> Vec<String> {
let (_, env_idx) = parse_env_flag(args);
let mut result = Vec::with_capacity(args.len());
for (i, arg) in args.iter().enumerate() {
if i == env_idx || (i == env_idx + 1 && args.get(env_idx) == Some(&"--env".to_string())) {
continue;
}
result.push(arg.clone());
}
result
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args: Vec<String> = env::args().collect();
let (env, _) = parse_env_flag(&args);
if args.len() < 2 {
print_usage(&args[0]);
std::process::exit(1);
@@ -244,32 +275,35 @@ async fn main() -> anyhow::Result<()> {
match args[1].as_str() {
"import" => {
if args.len() != 3 {
eprintln!("Användning: {} import <csv-fil>", args[0]);
let clean_args = remove_env_flags(&args);
if clean_args.len() != 3 {
eprintln!("Usage: {} import <csv-file> [--env <name>]", clean_args[0]);
std::process::exit(1);
}
let csv_path = PathBuf::from(&args[2]);
let csv_path = PathBuf::from(&clean_args[2]);
if !csv_path.exists() {
eprintln!("Fel: Filen hittades inte: {:?}", csv_path);
eprintln!("Error: File not found: {:?}", csv_path);
std::process::exit(1);
}
let config = Config::load()?;
println!("Environment: {}", env.as_str());
let config = Config::load(env)?;
let pool = create_pool(&config.database.connection_url()).await?;
let repo = Repository::new(pool);
commands::run_import(&csv_path, &repo).await?;
}
"generate" => {
if args.len() != 3 {
eprintln!("Användning: {} generate <csv-fil> <utdatakatalog>", args[0]);
let clean_args = remove_env_flags(&args);
if clean_args.len() != 4 {
eprintln!("Usage: {} generate <csv-file> <output-dir> [--env <name>]", clean_args[0]);
std::process::exit(1);
}
let input_path = Path::new(&args[2]);
let base_output_dir = Path::new(&args[3]);
let input_path = Path::new(&clean_args[2]);
let base_output_dir = Path::new(&clean_args[3]);
if !input_path.exists() {
eprintln!("Fel: Filen hittades inte: {:?}", input_path);
eprintln!("Error: File not found: {:?}", input_path);
std::process::exit(1);
}
@@ -279,7 +313,7 @@ async fn main() -> anyhow::Result<()> {
.unwrap_or("unknown")
.to_string();
println!("Konverterar {} till rensat format...", filename);
println!("Converting {} to cleaned format...", filename);
let temp_cleaned_path =
base_output_dir.join(format!("{}.temp.csv", filename.trim_end_matches(".txt")));
@@ -295,7 +329,7 @@ async fn main() -> anyhow::Result<()> {
)?;
println!(
"Konverterade {} transaktioner",
"Converted {} transactions",
fs::read_to_string(output_dir.join(format!("{}.csv", batch_number)))?
.lines()
.count()
@@ -303,7 +337,7 @@ async fn main() -> anyhow::Result<()> {
);
let batch = read_csv_file(&output_dir.join(format!("{}.csv", batch_number)))?;
println!("Laddade {} transaktioner", batch.transactions.len());
println!("Loaded {} transactions", batch.transactions.len());
let first_date = batch.transactions.first().map(|t| t.date).unwrap();
let last_date = batch.transactions.last().map(|t| t.date).unwrap();
@@ -343,19 +377,45 @@ async fn main() -> anyhow::Result<()> {
.unwrap();
let filename = format!("customer_{}.html", customer_num);
fs::write(output_dir.join(&filename), customer_html)?;
println!("Genererade {}", filename);
println!("Generated {}", filename);
}
println!(
"\nGenererade {} kundfakturor i {:?}",
"\nGenerated {} customer invoices in {:?}",
customer_count, output_dir
);
}
"db" => {
let clean_args = remove_env_flags(&args);
if clean_args.len() < 3 {
eprintln!("Usage: {} db <subcommand> [--env <name>]", clean_args[0]);
eprintln!("Subcommands:");
eprintln!(" setup Create database and schema");
std::process::exit(1);
}
println!("Environment: {}", env.as_str());
let config = Config::load(env)?;
let pool = create_pool(&config.database.connection_url()).await?;
let repo = Repository::new(pool);
match clean_args[2].as_str() {
"setup" => {
commands::run_db_setup(&repo, &config).await?;
}
_ => {
eprintln!("Unknown db subcommand: {}", clean_args[2]);
eprintln!("Subcommands:");
eprintln!(" setup Create database and schema");
std::process::exit(1);
}
}
}
"help" | "--help" | "-h" => {
print_usage(&args[0]);
}
_ => {
eprintln!("Okänt kommando: {}", args[1]);
eprintln!("Unknown command: {}", args[1]);
print_usage(&args[0]);
std::process::exit(1);
}
@@ -365,10 +425,13 @@ async fn main() -> anyhow::Result<()> {
}
fn print_usage(program: &str) {
eprintln!("Användning: {} <kommando> [argument]", program);
eprintln!("Usage: {} <command> [arguments]", program);
eprintln!();
eprintln!("Kommandon:");
eprintln!(" import <csv-fil> Importera CSV-data till databasen");
eprintln!(" generate <csv-fil> <dir> Generera HTML-fakturor från CSV");
eprintln!(" help Visa denna hjälptext");
eprintln!("Commands:");
eprintln!(" import <csv-file> [--env <name>] Import CSV data to database (default: prod)");
eprintln!(" generate <csv> <dir> Generate HTML invoices from CSV");
eprintln!(" db setup [--env <name>] Create database and schema (default: prod)");
eprintln!(" help Show this help message");
eprintln!();
eprintln!("Environments: prod (default), dev, test");
}