tktax-config 0.2.2

Configuration handling for tktax, integrating environment- and file-based settings.
Documentation
// ---------------- [ File: tktax-config/src/program_config.rs ]
crate::ix!();

#[derive(Getters,Default,Debug,Clone,Deserialize)]
#[getset(get="pub")]
pub struct AccountsConfig {
    #[serde(default)] checking: Option<HashMap<u32, String>>,
    #[serde(default)] savings:  Option<HashMap<u32, String>>,
    #[serde(default)] credit:   Option<HashMap<u32, String>>,
}

#[derive(Getters,Debug,Clone,Deserialize)]
#[getset(get="pub")]
pub struct GeneralConfig {

    #[serde(default)]
    long: bool,
}

impl Default for GeneralConfig {

    fn default() -> Self {
        Self {
            long: true,
        }
    }
}

//------------------------------------------------------
#[derive(Getters,Clone,Debug,Deserialize)]
#[getset(get="pub")]
pub struct ProgramConfig {

    #[serde(flatten)]
    #[serde(default)]
    general: GeneralConfig,

    accounts: Option<AccountsConfig>,
    amazon:   Option<AmazonConfig>,

    // ← Keep track of where config.toml was loaded from:
    config_dir: PathBuf, // not exposed publicly
}

impl ProgramConfig {

    pub fn create_amazon_item_map(&self) -> Option<AmazonItemMap> 
    {
        if self.amazon().is_none() {
            return None;
        }

        let amazon = self.amazon().as_ref().unwrap();

        create_amazon_item_map(amazon)
    }

    // Now, whenever you need to open a CSV file from accounts, you do:
    pub fn csv_path_for(&self, relative_path: &str) -> PathBuf {
        self.config_dir.join(relative_path)
    }
}

// Because we can’t easily derive(Deserialize) on ProgramConfig with that extra field:
#[derive(Debug, Deserialize)]
struct ProgramConfigIntermediate {
    general:  GeneralConfig,
    accounts: Option<AccountsConfig>,
    amazon:   Option<AmazonConfig>,
}

pub fn load_program_config_from_path<P: AsRef<Path> + Debug>(
    config_path: P
) -> Result<ProgramConfig, ConfigError> {

    let config_path = config_path.as_ref().canonicalize()
        .expect(&format!("could not canonicalize {:?}", config_path));

    let config_dir = config_path
        .parent()
        .unwrap_or_else(|| Path::new(".")) // fallback: current dir
        .to_path_buf();

    // 1) Build config the usual way
    let builder = ConfigConfig::builder()
        .add_source(ConfigFile::from(config_path.as_ref()))
        .add_source(ConfigEnvironment::with_prefix("TKTAX"));

    // 2) Deserialize first into an intermediate `tmp` variable
    let tmp = builder.build()?.try_deserialize::<ProgramConfigIntermediate>()?;

    // 3) Then wrap in your real `ProgramConfig` with `config_dir` injected
    Ok(ProgramConfig {
        general: tmp.general,
        accounts: tmp.accounts,
        amazon: tmp.amazon,
        config_dir,
    })
}

impl Default for ProgramConfig {
    fn default() -> Self {
        let crate_root = std::env::var("CARGO_MANIFEST_DIR")
            .unwrap_or_else(|_| ".".into()); // Fallback to current dir if missing

        // E.g. if your config is in the root of *this crate*, do:
        let config_path = PathBuf::from(&crate_root).join("config.toml");

        // If your config is truly in the *workspace* root (one level above),
        // you might do:
        // let config_path = PathBuf::from(&crate_root).join("..").join("config.toml");

        match load_program_config_from_path(&config_path) {
            Ok(cfg) => cfg,
            Err(e) => {
                // do whatever is appropriate in your situation.
                // For demonstration, just panic here:
                panic!("Failed to load ProgramConfig from {:?}: {:#}", config_path, e)
            }
        }
    }
}