use crate::config::{Config, ConfigInput, DevUrlArgs, TargetUrlArgs};
use anyhow::{Result, anyhow};
use serde_json;
use serde_yaml;
use std::path::Path;
#[derive(Debug, Clone, clap::Subcommand)]
pub enum ConfigCommands {
Get {
key: String,
#[arg(long, default_value = "text")]
format: OutputFormat,
},
Set {
key: String,
value: String,
#[arg(long, default_value = "pgmt.yaml")]
config_file: String,
},
List {
#[arg(long, default_value = "yaml")]
format: OutputFormat,
},
Validate {
#[arg(long, default_value = "pgmt.yaml")]
config_file: String,
},
}
#[derive(Debug, Clone, clap::ValueEnum)]
pub enum OutputFormat {
Text,
Json,
Yaml,
}
pub async fn cmd_config(
config: &Config,
file_input: &ConfigInput,
subcommand: Option<ConfigCommands>,
) -> Result<()> {
match subcommand {
Some(ConfigCommands::Get { key, format }) => {
let value = get_config_value(config, file_input, &key)?;
print_value(&value, &format);
Ok(())
}
Some(ConfigCommands::Set {
key,
value,
config_file,
}) => {
set_config_value(&config_file, &key, &value)?;
println!("✅ Configuration updated: {} = {}", key, value);
Ok(())
}
Some(ConfigCommands::List { format }) => {
list_config_values(config, file_input, &format)?;
Ok(())
}
Some(ConfigCommands::Validate { config_file }) => {
validate_config_file(&config_file)?;
println!("✅ Configuration file '{}' is valid", config_file);
Ok(())
}
None => {
println!("pgmt config - Manage pgmt configuration");
println!();
println!("Usage:");
println!(" pgmt config get <KEY> Get a configuration value");
println!(" pgmt config set <KEY> <VALUE> Set a configuration value");
println!(" pgmt config list List all configuration values");
println!(" pgmt config validate Validate configuration file");
println!();
println!("Examples:");
println!(" pgmt config get databases.dev");
println!(" pgmt config set migration.tracking_table.name my_migrations");
println!(" pgmt config list --format json");
Ok(())
}
}
}
fn get_config_value(config: &Config, file_input: &ConfigInput, key: &str) -> Result<String> {
let parts: Vec<&str> = key.split('.').collect();
match parts.as_slice() {
["databases", "dev"] => Ok(DevUrlArgs::default()
.lookup(file_input)
.unwrap_or_else(|| "(not set)".to_string())),
["databases", "target"] => Ok(TargetUrlArgs::default()
.lookup(file_input)
.unwrap_or_else(|| "(not set)".to_string())),
["directories", "schema"] => Ok(config.directories.schema.clone()),
["directories", "migrations"] => Ok(config.directories.migrations.clone()),
["directories", "baselines"] => Ok(config.directories.baselines.clone()),
["directories", "roles"] => Ok(config.directories.roles.clone()),
["migration", "default_mode"] => Ok(config.migration.default_mode.clone()),
["migration", "validate_baseline_consistency"] => {
Ok(config.migration.validate_baseline_consistency.to_string())
}
["migration", "create_baselines_by_default"] => {
Ok(config.migration.create_baselines_by_default.to_string())
}
["migration", "tracking_table", "schema"] => {
Ok(config.migration.tracking_table.schema.clone())
}
["migration", "tracking_table", "name"] => Ok(config.migration.tracking_table.name.clone()),
["docker", "auto_cleanup"] => Ok(config.docker.auto_cleanup.to_string()),
["docker", "check_system_identifier"] => {
Ok(config.docker.check_system_identifier.to_string())
}
_ => Err(anyhow!("Unknown configuration key: {}", key)),
}
}
fn set_config_value(config_file: &str, key: &str, value: &str) -> Result<()> {
use crate::config::ConfigInput;
let config_path = Path::new(config_file);
if !config_path.exists() {
return Err(anyhow!("Configuration file '{}' not found", config_file));
}
let config_str = std::fs::read_to_string(config_path)?;
let mut config_input: ConfigInput = serde_yaml::from_str(&config_str)?;
let parts: Vec<&str> = key.split('.').collect();
match parts.as_slice() {
["databases", "dev"] => {
config_input
.databases
.get_or_insert_with(Default::default)
.dev_url = Some(value.to_string());
}
["databases", "target"] => {
config_input
.databases
.get_or_insert_with(Default::default)
.target_url = Some(value.to_string());
}
["directories", "schema"] => {
config_input
.directories
.get_or_insert_with(Default::default)
.schema_dir = Some(value.to_string());
}
["directories", "migrations"] => {
config_input
.directories
.get_or_insert_with(Default::default)
.migrations_dir = Some(value.to_string());
}
["directories", "baselines"] => {
config_input
.directories
.get_or_insert_with(Default::default)
.baselines_dir = Some(value.to_string());
}
["migration", "default_mode"] => {
config_input
.migration
.get_or_insert_with(Default::default)
.default_mode = Some(value.to_string());
}
["migration", "validate_baseline_consistency"] => {
let bool_val = value
.parse::<bool>()
.map_err(|_| anyhow!("Invalid boolean value: {}", value))?;
config_input
.migration
.get_or_insert_with(Default::default)
.validate_baseline_consistency = Some(bool_val);
}
["migration", "create_baselines_by_default"] => {
let bool_val = value
.parse::<bool>()
.map_err(|_| anyhow!("Invalid boolean value: {}", value))?;
config_input
.migration
.get_or_insert_with(Default::default)
.create_baselines_by_default = Some(bool_val);
}
["migration", "tracking_table", "schema"] => {
config_input
.migration
.get_or_insert_with(Default::default)
.tracking_table
.get_or_insert_with(Default::default)
.schema = Some(value.to_string());
}
["migration", "tracking_table", "name"] => {
config_input
.migration
.get_or_insert_with(Default::default)
.tracking_table
.get_or_insert_with(Default::default)
.name = Some(value.to_string());
}
["docker", "auto_cleanup"] => {
let bool_val = value
.parse::<bool>()
.map_err(|_| anyhow!("Invalid boolean value: {}", value))?;
config_input
.docker
.get_or_insert_with(Default::default)
.auto_cleanup = Some(bool_val);
}
["docker", "check_system_identifier"] => {
let bool_val = value
.parse::<bool>()
.map_err(|_| anyhow!("Invalid boolean value: {}", value))?;
config_input
.docker
.get_or_insert_with(Default::default)
.check_system_identifier = Some(bool_val);
}
_ => return Err(anyhow!("Unknown or unsupported configuration key: {}", key)),
}
let yaml_str = serde_yaml::to_string(&config_input)?;
std::fs::write(config_path, yaml_str)?;
Ok(())
}
fn list_config_values(
config: &Config,
file_input: &ConfigInput,
format: &OutputFormat,
) -> Result<()> {
let config_map = serde_json::json!({
"databases": {
"dev": DevUrlArgs::default().lookup(file_input),
"target": TargetUrlArgs::default().lookup(file_input),
},
"directories": {
"schema": config.directories.schema,
"migrations": config.directories.migrations,
"baselines": config.directories.baselines,
"roles": config.directories.roles,
},
"migration": {
"default_mode": config.migration.default_mode,
"validate_baseline_consistency": config.migration.validate_baseline_consistency,
"create_baselines_by_default": config.migration.create_baselines_by_default,
"tracking_table": {
"schema": config.migration.tracking_table.schema,
"name": config.migration.tracking_table.name,
}
},
"objects": {
"include": {
"schemas": config.objects.include.schemas,
"tables": config.objects.include.tables,
},
"exclude": {
"schemas": config.objects.exclude.schemas,
"tables": config.objects.exclude.tables,
}
},
"docker": {
"auto_cleanup": config.docker.auto_cleanup,
"check_system_identifier": config.docker.check_system_identifier,
}
});
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&config_map)?);
}
OutputFormat::Yaml => {
println!("{}", serde_yaml::to_string(&config_map)?);
}
OutputFormat::Text => {
println!("Current Configuration:");
println!();
print_config_text(&config_map, 0);
}
}
Ok(())
}
fn print_config_text(value: &serde_json::Value, indent: usize) {
let prefix = " ".repeat(indent);
match value {
serde_json::Value::Object(map) => {
for (key, val) in map {
if val.is_object() {
println!("{}{}:", prefix, key);
print_config_text(val, indent + 1);
} else if val.is_array() {
println!("{}{}: {:?}", prefix, key, val);
} else {
println!("{}{}: {}", prefix, key, val);
}
}
}
_ => {
println!("{}{}", prefix, value);
}
}
}
fn validate_config_file(config_file: &str) -> Result<()> {
use crate::config::{ConfigBuilder, ConfigInput};
let config_path = Path::new(config_file);
if !config_path.exists() {
return Err(anyhow!("Configuration file '{}' not found", config_file));
}
let config_str = std::fs::read_to_string(config_path)?;
let config_input: ConfigInput =
serde_yaml::from_str(&config_str).map_err(|e| anyhow!("Invalid YAML syntax: {}", e))?;
let _resolved = ConfigBuilder::new()
.with_file(config_input)
.resolve()
.map_err(|e| anyhow!("Configuration validation failed: {}", e))?;
Ok(())
}
fn print_value(value: &str, format: &OutputFormat) {
match format {
OutputFormat::Text => println!("{}", value),
OutputFormat::Json => {
let json_val = serde_json::json!(value);
println!("{}", json_val);
}
OutputFormat::Yaml => {
println!("{}", value);
}
}
}