konfig_rust/
lib.rs

1pub mod format;
2
3use crate::format::*;
4use std::borrow::Cow;
5use std::collections::HashMap;
6use std::error::Error;
7use std::fs::{read, File};
8use std::io::Write;
9use std::path::Path;
10use std::ptr::NonNull;
11use std::str;
12use thiserror::Error;
13
14/// This is the error enum for konfig-rust, it contains all possible errors returned by konfig-rust
15///
16/// Always make sure to check for relevant error types
17#[derive(Debug, Error)]
18pub enum KonfigError {
19    #[error("Validation callback error {0}")]
20    ValidationError(String),
21    #[error("OnLoad callback error {0}")]
22    OnLoadError(String),
23    #[error("Marshal error {0}")]
24    MarshalError(String),
25    #[error("Unmarshal error {0}")]
26    UnmarshalError(String),
27    #[error("Load error {0}")]
28    LoadError(String),
29    #[error("Save error {0}")]
30    SaveError(String),
31    #[error("Registration error {0}")]
32    RegistrationError(String),
33    #[error(transparent)]
34    Other(#[from] Box<dyn Error>),
35}
36
37/// This is the trait allowing a struct to be registered as a section and managed by `KonfigManager`
38///
39/// You can use the shorthand `#[derive(KonfigSection)]` macro to implement automatically implement it using defaults
40///
41/// If you want to use custom validate or on_load callbacks, you have to implement this trait manually,
42/// there are plans to allow for adding custom callbacks using the derive macro or with a functional interface
43pub trait KonfigSection {
44    fn name(&self) -> Cow<'_, str>;
45    fn validate(&self) -> Result<(), KonfigError> {
46        Ok(())
47    }
48    fn on_load(&self) -> Result<(), KonfigError> {
49        Ok(())
50    }
51    fn to_bytes(&self, format: &FormatHandlerEnum) -> Result<Vec<u8>, KonfigError>;
52    fn update_from_bytes(
53        &mut self,
54        bytes: &[u8],
55        format: &FormatHandlerEnum,
56    ) -> Result<(), KonfigError>;
57}
58
59// had to go into unsafe land to deliver dx 🤷
60struct SectionPtr {
61    ptr: NonNull<dyn KonfigSection>,
62}
63
64unsafe impl Send for SectionPtr {}
65unsafe impl Sync for SectionPtr {}
66
67impl SectionPtr {
68    fn new<T: KonfigSection + 'static>(section: &mut T) -> Self {
69        // SAFETY: We're creating a pointer to an object we know exists
70        // The caller must ensure the object outlives all uses of this pointer
71        let ptr = unsafe { NonNull::new_unchecked(section as *mut T as *mut dyn KonfigSection) };
72        SectionPtr { ptr }
73    }
74
75    unsafe fn as_ref(&self) -> &dyn KonfigSection {
76        unsafe { self.ptr.as_ref() }
77    }
78
79    unsafe fn as_mut(&mut self) -> &mut dyn KonfigSection {
80        unsafe { self.ptr.as_mut() }
81    }
82}
83
84/// Used for configuring `KonfigManager`
85pub struct KonfigOptions {
86    /// The format KonfigManager is supposed to use for the config file, possible options are in the `Format` enum
87    pub format: Format,
88    // If `true`, KonfigManager will try to load the config file when it is created
89    // auto_load: bool,
90    /// If `true` will try to save data to the config file on panic and SIGINT and SIGTERM (currently noop due to rust lifetime issues)
91    pub auto_save: bool,
92    /// Whether to call the validate and on_load callbacks when loading the data from file
93    pub use_callbacks: bool,
94    /// Path to the file used for configuration, if the file does not exist it will be created,
95    /// the path can be absolute or relative
96    pub config_path: String,
97}
98
99/// The main manager in konfig-rust, this is intended to be created near the start of your program, and destroyed by closing it
100///
101/// `KonfigManager` allows you to register sections, which then can be loaded and saved into a single file
102///
103/// ## Keep in mind this uses raw pointers to store data section, hence why it is supposed to be used, as a single "source of truth" in your app, likely as a global instance
104///
105/// example:
106/// ```
107/// use serde::{Deserialize, Serialize};
108/// use konfig_rust::*;
109/// use konfig_rust::format::*;
110///
111/// use konfig_rust_derive::KonfigSection;
112///
113/// #[derive(Serialize, Deserialize, KonfigSection)] // Aside from KonfigSection, you also have to use the Serialize and Deserialize macros
114/// struct Config {
115///     name: String,
116///     age: u32,
117/// }
118///
119/// let mut c = Config { name: "Bob".to_string(), age: 32 };
120///
121/// let mut manager = KonfigManager::new(KonfigOptions {
122///     format: Format::JSON,
123///     auto_save: true,
124///     use_callbacks: true,
125///     config_path: "config.json".to_string(),
126/// });
127///
128/// manager.register_section(&mut c).unwrap();
129///
130/// manager.load().unwrap();
131///
132/// println!("Name: {}, Age: {}", c.name, c.age); // Notice how you just access the struct like normal in memory state storage
133///
134/// manager.save().unwrap();
135/// ```
136pub struct KonfigManager {
137    opts: KonfigOptions,
138    format_handler: FormatHandlerEnum,
139    path: Box<Path>,
140    sections: HashMap<String, SectionPtr>,
141}
142
143// lazy_static! {
144//         static ref SAVE_CLOSURE: Mutex<Vec<Box< FnOnce() + Send() + 'static>>> = Mutex::new(Vec::new());
145// }
146
147impl KonfigManager {
148    /// Simply creates a new `KonfigManager`, with the passed in `KonfigOptions`
149    pub fn new(opts: KonfigOptions) -> Self {
150        let m = KonfigManager {
151            format_handler: opts.format.create_handler(),
152            path: Box::from(Path::new(&opts.config_path)),
153            opts,
154            sections: HashMap::new(),
155        };
156
157        // probably just gonna rawdog pointers here to, couse rust cries too much about it
158        if m.opts.auto_save {
159            // setup panic hook
160            // let prev_hook = panic::take_hook();
161            // panic::set_hook(Box::new(move |panic_info| {
162            //     &m.save().unwrap();
163            //     prev_hook(panic_info);
164            // }));
165
166            // TODO: setup fully later
167            // setup SIGINT and SIGTERM
168            // let mut signals = Signals::new(&[signal_hook::consts::SIGINT, signal_hook::consts::SIGTERM]).unwrap();
169
170        }
171
172        m
173    }
174
175    /// Loads the found config data from the specified file into the registered sections
176    ///
177    /// Throws: `KonfigError::LoadError`
178    pub fn load(&mut self) -> Result<(), KonfigError> {
179        if File::open(&self.path).is_err() {
180            File::create(&self.path).map_err(|err| KonfigError::LoadError(err.to_string()))?;
181        }
182        let data = read(&self.path).map_err(|err| KonfigError::LoadError(err.to_string()))?;
183
184        if data.is_empty() {
185            return Ok(());
186        }
187
188        let config: serde_json::Value = match &self.format_handler {
189            FormatHandlerEnum::JSON(handler) => handler.unmarshal(data.as_slice())?,
190            FormatHandlerEnum::YAML(handler) => handler.unmarshal(data.as_slice())?,
191            FormatHandlerEnum::TOML(handler) => handler.unmarshal(data.as_slice())?,
192        };
193
194        let config_map = config
195            .as_object()
196            .ok_or_else(|| KonfigError::LoadError("Config root must be an object".to_string()))?;
197
198        for (name, section_value) in config_map {
199            if let Some(section_ptr) = self.sections.get_mut(name) {
200                let bytes = self.format_handler.marshal(section_value)?;
201                unsafe {
202                    let section = section_ptr.as_mut();
203                    section.update_from_bytes(&bytes, &self.format_handler)?;
204                    if self.opts.use_callbacks {
205                        section.validate()?;
206                        section.on_load()?;
207                    }
208                }
209            }
210        }
211
212        Ok(())
213    }
214
215    // fn internal_save(&self) {
216    //     let mut closures = SAVE_CLOSURE.lock().unwrap();
217    //     while let Some(closure) = closures.pop() {
218    //         closure();
219    //     }
220    // }
221
222    /// Saves the registered sections to the specified file
223    ///
224    /// Throws: `KonfigError::SaveError`
225    pub fn save(&self) -> Result<(), KonfigError> {
226        let mut map: HashMap<String, serde_json::Value> = HashMap::new();
227
228        for (name, section_ptr) in &self.sections {
229            let section = unsafe { section_ptr.as_ref() };
230            let bytes = section.to_bytes(&self.format_handler)?;
231
232            let value: serde_json::Value = match &self.format_handler {
233                FormatHandlerEnum::JSON(_) => serde_json::from_slice(&bytes)
234                    .map_err(|err| KonfigError::UnmarshalError(err.to_string()))?,
235                FormatHandlerEnum::YAML(_) => serde_yaml::from_slice(&bytes)
236                    .map_err(|err| KonfigError::UnmarshalError(err.to_string()))?,
237                FormatHandlerEnum::TOML(_) => {
238                    let s = str::from_utf8(&bytes)
239                        .map_err(|err| KonfigError::UnmarshalError(err.to_string()))?;
240                    toml::from_str(s).map_err(|err| KonfigError::UnmarshalError(err.to_string()))?
241                }
242            };
243
244            map.insert(name.clone(), value);
245        }
246
247        let out = self.format_handler.marshal(&map)?;
248
249        let mut f =
250            File::create(&self.path).map_err(|err| KonfigError::SaveError(err.to_string()))?;
251
252        f.write_all(out.as_slice())
253            .map_err(|err| KonfigError::SaveError(err.to_string()))?;
254
255        Ok(())
256    }
257
258    /// Registers a new section with the KonfigManager, the section must use the `Serialize` and `Deserialize` macros
259    /// and implement the `KonfigSection` trait (or use the `#[derive(KonfigSection)]` macro to do it for you)
260    ///
261    /// Throws: `KonfigError::RegistrationError`
262    pub fn register_section<T>(&mut self, section: &mut T) -> Result<(), KonfigError>
263    where
264        T: KonfigSection + 'static,
265    {
266        let name = section.name().to_string();
267
268        if self.sections.contains_key(&name) {
269            return Err(KonfigError::RegistrationError(format!(
270                "Failed to register {}, section already registered",
271                name
272            )));
273        }
274
275        let section_ptr = SectionPtr::new(section);
276
277        self.sections.insert(name, section_ptr);
278        Ok(())
279    }
280
281    /// Quick helper that runs the validation callback on all registered sections,
282    /// useful if the config is modified a lot in memory but rarely saved or loaded
283    pub fn validate_all(&self) -> Vec<(String, Result<(), KonfigError>)> {
284        self.sections
285            .iter()
286            .map(|(name, section_ptr)| {
287                let result = unsafe { section_ptr.as_ref().validate() };
288                (name.clone(), result)
289            })
290            .collect()
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use konfig_rust_derive::KonfigSection;
298    use serde::{Deserialize, Serialize};
299
300    #[test]
301    fn test_konfig() {
302        #[derive(Serialize, Deserialize, KonfigSection)]
303        struct TestData {
304            a: i32,
305            b: String,
306        }
307
308        #[derive(Serialize, Deserialize, KonfigSection)]
309        struct TestData2 {
310            port: String,
311            host: String,
312        }
313
314        let mut t = TestData {
315            a: 1,
316            b: "test".to_string(),
317        };
318
319        let mut t2 = TestData2 {
320            port: "8080".to_string(),
321            host: "localhost".to_string(),
322        };
323
324        let mut mngr = KonfigManager::new(KonfigOptions {
325            format: Format::JSON,
326            auto_save: false,
327            use_callbacks: true,
328            config_path: "test.json".to_string(),
329        });
330
331        mngr.register_section(&mut t)
332            .map_err(|err| println!("{}", err.to_string()))
333            .unwrap();
334
335        mngr.register_section(&mut t2)
336            .map_err(|err| println!("{}", err.to_string()))
337            .unwrap();
338
339        mngr.load()
340            .map_err(|err| println!("{}", err.to_string()))
341            .unwrap();
342
343        t.a = t.a + 1;
344
345        mngr.save()
346            .map_err(|err| println!("{}", err.to_string()))
347            .unwrap();
348
349        for (name, result) in mngr.validate_all() {
350            println!("{}: {}", name, result.is_ok());
351        }
352        println!("TestData: {}, {}", t.a, t.b);
353        println!("TestData2: {}, {}", t2.port, t2.host);
354    }
355}