use super::Config;
use crate::error::{MeiliBridgeError, Result};
use config::{Config as ConfigBuilder, Environment, File};
use std::env;
use std::path::Path;
pub struct ConfigLoader;
impl ConfigLoader {
pub fn load() -> Result<Config> {
let mut builder = ConfigBuilder::builder();
if let Ok(config_path) = env::var("CONFIG_PATH") {
builder = builder.add_source(File::with_name(&config_path));
} else {
let config_files = [
"config.yaml",
"config.yml",
"meilibridge.yaml",
"meilibridge.yml",
];
for file in &config_files {
if Path::new(file).exists() {
builder = builder.add_source(File::with_name(file));
break;
}
}
}
if let Ok(env_name) = env::var("APP_ENV") {
let env_configs = [
format!("config.{}.yaml", env_name),
format!("config.{}.yml", env_name),
format!("{}.yaml", env_name),
format!("{}.yml", env_name),
];
for file in &env_configs {
if Path::new(file).exists() {
builder = builder.add_source(File::with_name(file));
break;
}
}
}
builder = builder.add_source(
Environment::with_prefix("MEILIBRIDGE")
.separator("__")
.try_parsing(true),
);
let config = builder
.build()
.map_err(|e| MeiliBridgeError::Config(format!("Failed to build config: {}", e)))?;
let config: Config = config.try_deserialize().map_err(|e| {
MeiliBridgeError::Config(format!("Failed to deserialize config: {}", e))
})?;
Self::validate(&config)?;
Ok(config)
}
pub fn validate(config: &Config) -> Result<()> {
let mut errors = Vec::new();
if let Some(super::SourceConfig::PostgreSQL(pg)) = &config.source {
if pg.pool.max_size < pg.pool.min_idle {
errors.push("PostgreSQL pool max_size must be >= min_idle".to_string());
}
if pg.pool.max_size == 0 {
errors.push("PostgreSQL pool max_size must be > 0".to_string());
}
}
for named_source in &config.sources {
if let super::SourceConfig::PostgreSQL(pg) = &named_source.config {
if pg.pool.max_size < pg.pool.min_idle {
errors.push(format!(
"PostgreSQL source '{}' pool max_size must be >= min_idle",
named_source.name
));
}
if pg.pool.max_size == 0 {
errors.push(format!(
"PostgreSQL source '{}' pool max_size must be > 0",
named_source.name
));
}
}
}
if config.sync_tasks.is_empty() {
errors.push("At least one sync task must be configured".to_string());
}
for (i, task) in config.sync_tasks.iter().enumerate() {
if task.table.is_empty() {
errors.push(format!("Sync task {} has empty table name", i));
}
if task.index.is_empty() {
errors.push(format!("Sync task {} has empty index name", i));
}
if task.options.batch_size == 0 {
errors.push(format!("Sync task {} batch_size must be > 0", i));
}
}
if config.meilisearch.url.is_empty() {
errors.push("Meilisearch URL cannot be empty".to_string());
}
if config.redis.url.is_empty() {
errors.push("Redis URL cannot be empty".to_string());
}
if config.api.port == 0 {
errors.push("API port must be > 0".to_string());
}
if !errors.is_empty() {
return Err(MeiliBridgeError::Validation(errors.join(", ")));
}
Ok(())
}
pub fn load_from_file(path: &str) -> Result<Config> {
env::set_var("CONFIG_PATH", path);
Self::load()
}
pub fn generate_sample() -> &'static str {
r#"# MeiliBridge Configuration Example
# Copy this file to config.yaml and adjust for your environment
app:
name: meilibridge-dev
# instance_id: auto # Automatically generated if not specified
tags:
environment: development
region: local
# PostgreSQL source configuration
source:
type: postgresql
host: localhost
port: 5432
database: myapp
username: postgres
password: ${POSTGRES_PASSWORD} # Use environment variable
slot_name: meilibridge_slot
publication: meilibridge_pub
pool:
max_size: 10
min_idle: 1
ssl:
mode: prefer
# Meilisearch configuration
meilisearch:
url: http://localhost:7700
api_key: ${MEILISEARCH_API_KEY} # Use environment variable
timeout: 30
max_connections: 10
index_settings:
searchable_attributes:
- title
- description
- content
filterable_attributes:
- category
- status
- created_at
# Redis configuration for state management
redis:
url: redis://localhost:6379
# password: ${REDIS_PASSWORD} # Uncomment if Redis requires auth
database: 0
key_prefix: meilibridge
# Sync task definitions
sync_tasks:
- table: users
index: users
primary_key: id
field_mappings:
user_id: id
full_name: name
email_address: email
filters:
exclude:
- password_hash
- internal_notes
options:
batch_size: 1000
batch_timeout: 10
- table: products
index: products
primary_key: sku
full_sync: true
filters:
expressions:
- field: status
operator: eq
value: "active"
options:
batch_size: 5000
# API server configuration
api:
host: 127.0.0.1
port: 7701
cors:
enabled: true
allowed_origins:
- http://localhost:3000
auth:
enabled: false
# jwt_secret: ${JWT_SECRET}
# token_expiry: 3600
# Logging configuration
logging:
level: info
format: text # text, json, or pretty
outputs:
- type: stdout
# - type: file
# path: ./logs/meilibridge.log
# rotation: daily
# Feature flags
features:
auto_recovery: true
health_checks: true
metrics_export: true
distributed_mode: false
"#
}
}