Skip to main content

kas_core/config/
factory.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Configuration options
7
8#[cfg(feature = "serde")] use super::Format;
9use super::{Config, Error};
10#[cfg(feature = "serde")]
11use crate::util::warn_about_error_with_path;
12use std::cell::RefCell;
13#[cfg(feature = "serde")] use std::path::PathBuf;
14use std::rc::Rc;
15
16/// A factory able to source and (optionally) save [`Config`]
17pub trait ConfigFactory {
18    /// Construct a [`Config`] object
19    ///
20    /// Returning an [`Error`] here will prevent startup of the UI. As such,
21    /// it may be preferable to return [`Config::default()`] than to fail.
22    fn read_config(&mut self) -> Result<Rc<RefCell<Config>>, Error>;
23
24    /// Return optional config-writing fn
25    fn writer(self) -> Option<Box<dyn FnMut(&Config)>>;
26}
27
28/// Always use default [`Config`]
29#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
30pub struct DefaultFactory;
31
32impl ConfigFactory for DefaultFactory {
33    fn read_config(&mut self) -> Result<Rc<RefCell<Config>>, Error> {
34        Ok(Rc::new(RefCell::new(Config::default())))
35    }
36
37    fn writer(self) -> Option<Box<dyn FnMut(&Config)>> {
38        None
39    }
40}
41
42/// Config mode
43///
44/// See [`ReadWriteFactory::from_env()`] documentation.
45#[cfg(feature = "serde")]
46#[derive(Clone, Debug, PartialEq, Eq, Hash)]
47pub enum ConfigMode {
48    /// Automatically determine mode based on the path
49    Auto,
50    /// Read-only mode
51    Read,
52    /// Read-write mode
53    ///
54    /// This mode reads config on start and writes changes on exit.
55    ReadWrite,
56    /// Use default config and write out
57    ///
58    /// This mode only writes initial (default) config and any writes changes on exit.
59    WriteDefault,
60}
61
62/// Read and write config from disk
63#[cfg(feature = "serde")]
64#[derive(Clone, Debug, PartialEq, Eq, Hash)]
65pub struct ReadWriteFactory {
66    path: PathBuf,
67    mode: ConfigMode,
68    fail_on_error: bool,
69}
70
71#[cfg(feature = "serde")]
72impl ReadWriteFactory {
73    /// Construct with specified `path` and `mode`
74    pub fn new(path: PathBuf, mode: ConfigMode) -> Self {
75        let fail_on_error = false;
76        ReadWriteFactory {
77            path,
78            mode,
79            fail_on_error,
80        }
81    }
82
83    /// Fail immediately in case of read error
84    ///
85    /// By default, [`Config::default`] will be returned on read error.
86    pub fn fail_on_error(mut self) -> Self {
87        self.fail_on_error = true;
88        self
89    }
90
91    /// Construct a new instance, reading from environment variables
92    ///
93    /// The following environment variables are read, in case-insensitive mode.
94    ///
95    /// # Config files
96    ///
97    /// WARNING: file formats are not stable and may not be compatible across
98    /// KAS versions (aside from patch versions)!
99    ///
100    /// The `KAS_CONFIG` variable, if given, provides a path to the KAS config
101    /// file, which is read or written according to `KAS_CONFIG_MODE`.
102    /// If `KAS_CONFIG` is not specified, platform-default configuration is used
103    /// without reading or writing. This may change to use a platform-specific
104    /// default path in future versions.
105    ///
106    /// The `KAS_CONFIG_MODE` variable determines the read/write mode:
107    ///
108    /// -   `Read`: read-only
109    /// -   `ReadWrite`: read on start-up, write on exit
110    /// -   `WriteDefault`: generate platform-default configuration and write
111    ///     it to the config path(s) specified, overwriting any existing config
112    /// -   If not specified the mode is automatically determined depending on
113    ///     what `path` resolves to.
114    ///
115    /// If `KAS_CONFIG_FAIL_ON_ERROR` is true, config read errors are fatal.
116    /// Otherwise default configuration will be used on read error.
117    pub fn from_env() -> Self {
118        use std::env::var;
119
120        let mut path = PathBuf::new();
121        let mut mode = ConfigMode::Auto;
122        let mut fail_on_error = false;
123
124        if let Ok(v) = var("KAS_CONFIG") {
125            path = v.into();
126        }
127
128        if let Ok(mut v) = var("KAS_CONFIG_MODE") {
129            v.make_ascii_uppercase();
130            mode = match v.as_str() {
131                "READ" => ConfigMode::Read,
132                "READWRITE" => ConfigMode::ReadWrite,
133                "WRITEDEFAULT" => ConfigMode::WriteDefault,
134                other => {
135                    log::error!("from_env: bad var KAS_CONFIG_MODE={other}");
136                    log::error!("from_env: supported config modes: READ, READWRITE, WRITEDEFAULT");
137                    mode
138                }
139            };
140        }
141
142        if let Ok(v) = var("KAS_CONFIG_FAIL_ON_ERROR") {
143            fail_on_error = match v.parse() {
144                Ok(b) => b,
145                _ => {
146                    log::error!("from_env: bad var KAS_CONFIG_FAIL_ON_ERROR={v}");
147                    true
148                }
149            };
150        }
151
152        ReadWriteFactory {
153            path,
154            mode,
155            fail_on_error,
156        }
157    }
158}
159
160#[cfg(feature = "serde")]
161impl ConfigFactory for ReadWriteFactory {
162    fn read_config(&mut self) -> Result<Rc<RefCell<Config>>, Error> {
163        let config = match self.mode {
164            _ if self.path.as_os_str().is_empty() => Config::default(),
165            ConfigMode::Auto | ConfigMode::Read | ConfigMode::ReadWrite => {
166                match Format::guess_and_read_path(&self.path) {
167                    Ok(config) => {
168                        self.mode = match std::fs::metadata(&self.path) {
169                            Ok(meta) if meta.is_file() && !meta.permissions().readonly() => {
170                                ConfigMode::ReadWrite
171                            }
172                            _ => ConfigMode::Read,
173                        };
174
175                        config
176                    }
177                    Err(error) => {
178                        if matches!(&error, Error::IoError(e) if e.kind() == std::io::ErrorKind::NotFound)
179                        {
180                            self.mode = ConfigMode::WriteDefault;
181                        } else {
182                            warn_about_error_with_path("failed to read config", &error, &self.path);
183                            if self.fail_on_error {
184                                return Err(error);
185                            }
186                        }
187                        Config::default()
188                    }
189                }
190            }
191            ConfigMode::WriteDefault => Default::default(),
192        };
193
194        if self.mode == ConfigMode::WriteDefault
195            && let Err(error) = Format::guess_and_write_path(&self.path, &config)
196        {
197            self.mode = ConfigMode::Read;
198            warn_about_error_with_path("failed to write default config: ", &error, &self.path);
199        }
200
201        Ok(Rc::new(RefCell::new(config)))
202    }
203
204    fn writer(self) -> Option<Box<dyn FnMut(&Config)>> {
205        if self.path.as_os_str().is_empty()
206            || matches!(self.mode, ConfigMode::Read | ConfigMode::ReadWrite)
207        {
208            return None;
209        }
210
211        let path = self.path;
212        Some(Box::new(move |config| {
213            if let Err(error) = Format::guess_and_write_path(&path, config) {
214                warn_about_error_with_path("failed to write config: ", &error, &path);
215            }
216        }))
217    }
218}
219
220/// A selected [`ConfigFactory`] implementation
221///
222/// This is a newtype over an implementation of [`ConfigFactory`], dependent on
223/// feature flags. Currently, this uses:
224///
225/// -   `cfg(feature = "serde")`: `ReadWriteFactory::from_env()`
226/// -   Otherwise: [`DefaultFactory::default()`]
227#[cfg(not(feature = "serde"))]
228#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
229pub struct AutoFactory(DefaultFactory);
230
231/// A selected [`ConfigFactory`] implementation
232///
233/// This is a newtype over an implementation of [`ConfigFactory`], dependent on
234/// feature flags. Currently, this uses:
235///
236/// -   `cfg(feature = "serde")`: [`ReadWriteFactory::from_env()`]
237/// -   Otherwise: [`DefaultFactory::default()`]
238#[cfg(feature = "serde")]
239#[derive(Clone, Debug, PartialEq, Eq, Hash)]
240pub struct AutoFactory(ReadWriteFactory);
241
242#[cfg(feature = "serde")]
243impl Default for AutoFactory {
244    fn default() -> Self {
245        AutoFactory(ReadWriteFactory::from_env())
246    }
247}
248
249impl ConfigFactory for AutoFactory {
250    #[inline]
251    fn read_config(&mut self) -> Result<Rc<RefCell<Config>>, Error> {
252        self.0.read_config()
253    }
254
255    #[inline]
256    fn writer(self) -> Option<Box<dyn FnMut(&Config)>> {
257        self.0.writer()
258    }
259}