tktax_config/
program_config.rs

1// ---------------- [ File: tktax-config/src/program_config.rs ]
2crate::ix!();
3
4#[derive(Getters,Default,Debug,Clone,Deserialize)]
5#[getset(get="pub")]
6pub struct AccountsConfig {
7    #[serde(default)] checking: Option<HashMap<u32, String>>,
8    #[serde(default)] savings:  Option<HashMap<u32, String>>,
9    #[serde(default)] credit:   Option<HashMap<u32, String>>,
10}
11
12#[derive(Getters,Debug,Clone,Deserialize)]
13#[getset(get="pub")]
14pub struct GeneralConfig {
15
16    #[serde(default)]
17    long: bool,
18}
19
20impl Default for GeneralConfig {
21
22    fn default() -> Self {
23        Self {
24            long: true,
25        }
26    }
27}
28
29//------------------------------------------------------
30#[derive(Getters,Clone,Debug,Deserialize)]
31#[getset(get="pub")]
32pub struct ProgramConfig {
33
34    #[serde(flatten)]
35    #[serde(default)]
36    general: GeneralConfig,
37
38    accounts: Option<AccountsConfig>,
39    amazon:   Option<AmazonConfig>,
40
41    // ← Keep track of where config.toml was loaded from:
42    config_dir: PathBuf, // not exposed publicly
43}
44
45impl ProgramConfig {
46
47    pub fn create_amazon_item_map(&self) -> Option<AmazonItemMap> 
48    {
49        if self.amazon().is_none() {
50            return None;
51        }
52
53        let amazon = self.amazon().as_ref().unwrap();
54
55        create_amazon_item_map(amazon)
56    }
57
58    // Now, whenever you need to open a CSV file from accounts, you do:
59    pub fn csv_path_for(&self, relative_path: &str) -> PathBuf {
60        self.config_dir.join(relative_path)
61    }
62}
63
64// Because we can’t easily derive(Deserialize) on ProgramConfig with that extra field:
65#[derive(Debug, Deserialize)]
66struct ProgramConfigIntermediate {
67    general:  GeneralConfig,
68    accounts: Option<AccountsConfig>,
69    amazon:   Option<AmazonConfig>,
70}
71
72pub fn load_program_config_from_path<P: AsRef<Path> + Debug>(
73    config_path: P
74) -> Result<ProgramConfig, ConfigError> {
75
76    let config_path = config_path.as_ref().canonicalize()
77        .expect(&format!("could not canonicalize {:?}", config_path));
78
79    let config_dir = config_path
80        .parent()
81        .unwrap_or_else(|| Path::new(".")) // fallback: current dir
82        .to_path_buf();
83
84    // 1) Build config the usual way
85    let builder = ConfigConfig::builder()
86        .add_source(ConfigFile::from(config_path.as_ref()))
87        .add_source(ConfigEnvironment::with_prefix("TKTAX"));
88
89    // 2) Deserialize first into an intermediate `tmp` variable
90    let tmp = builder.build()?.try_deserialize::<ProgramConfigIntermediate>()?;
91
92    // 3) Then wrap in your real `ProgramConfig` with `config_dir` injected
93    Ok(ProgramConfig {
94        general: tmp.general,
95        accounts: tmp.accounts,
96        amazon: tmp.amazon,
97        config_dir,
98    })
99}
100
101impl Default for ProgramConfig {
102    fn default() -> Self {
103        let crate_root = std::env::var("CARGO_MANIFEST_DIR")
104            .unwrap_or_else(|_| ".".into()); // Fallback to current dir if missing
105
106        // E.g. if your config is in the root of *this crate*, do:
107        let config_path = PathBuf::from(&crate_root).join("config.toml");
108
109        // If your config is truly in the *workspace* root (one level above),
110        // you might do:
111        // let config_path = PathBuf::from(&crate_root).join("..").join("config.toml");
112
113        match load_program_config_from_path(&config_path) {
114            Ok(cfg) => cfg,
115            Err(e) => {
116                // do whatever is appropriate in your situation.
117                // For demonstration, just panic here:
118                panic!("Failed to load ProgramConfig from {:?}: {:#}", config_path, e)
119            }
120        }
121    }
122}