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: KonfigSerialization {
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}
52
53/// Automatically implements internal methods originally defined in `KonfigSection`
54///
55/// only used internally by konfig
56pub trait KonfigSerialization {
57    fn to_bytes(&self, format: &FormatHandlerEnum) -> Result<Vec<u8>, KonfigError>;
58    fn update_from_bytes(
59        &mut self,
60        bytes: &[u8],
61        format: &FormatHandlerEnum,
62    ) -> Result<(), KonfigError>;
63}
64
65// blanket impl to simplify `KonfigSection`
66impl<T: ?Sized> KonfigSerialization for T
67where
68    T: serde::Serialize + serde::de::DeserializeOwned
69{
70    fn to_bytes(&self, format: &FormatHandlerEnum) -> Result<Vec<u8>, KonfigError> {
71        format.marshal(self)
72    }
73
74    fn update_from_bytes(&mut self, bytes: &[u8], format: &FormatHandlerEnum) -> Result<(), KonfigError> {
75        let new_instance: T = match format {
76            FormatHandlerEnum::JSON(_) => {
77                serde_json::from_slice(bytes)
78                    .map_err(|err| KonfigError::UnmarshalError(err.to_string()))?
79            },
80            FormatHandlerEnum::YAML(_) => {
81                serde_yaml::from_slice(bytes)
82                    .map_err(|err| KonfigError::UnmarshalError(err.to_string()))?
83            },
84            FormatHandlerEnum::TOML(_) => {
85                let s = str::from_utf8(bytes)
86                    .map_err(|err| KonfigError::UnmarshalError(err.to_string()))?;
87                toml::from_str(s)
88                    .map_err(|err| KonfigError::UnmarshalError(err.to_string()))?
89            },
90        };
91
92        *self = new_instance;
93        Ok(())
94    }
95}
96
97// had to go into unsafe land to deliver dx 🤷
98struct SectionPtr {
99    ptr: NonNull<dyn KonfigSection>,
100}
101
102unsafe impl Send for SectionPtr {}
103unsafe impl Sync for SectionPtr {}
104
105impl SectionPtr {
106    fn new<T: KonfigSection + 'static>(section: &mut T) -> Self {
107        // SAFETY: We're creating a pointer to an object we know exists
108        // The caller must ensure the object outlives all uses of this pointer
109        let ptr = unsafe { NonNull::new_unchecked(section as *mut T as *mut dyn KonfigSection) };
110        SectionPtr { ptr }
111    }
112
113    unsafe fn as_ref(&self) -> &dyn KonfigSection {
114        unsafe { self.ptr.as_ref() }
115    }
116
117    unsafe fn as_mut(&mut self) -> &mut dyn KonfigSection {
118        unsafe { self.ptr.as_mut() }
119    }
120}
121
122/// Used for configuring `KonfigManager`
123pub struct KonfigOptions {
124    /// The format KonfigManager is supposed to use for the config file, possible options are in the `Format` enum
125    pub format: Format,
126    // If `true`, KonfigManager will try to load the config file when it is created
127    // auto_load: bool,
128    /// If `true` will try to save data to the config file on panic and SIGINT and SIGTERM (currently noop due to rust lifetime issues)
129    pub auto_save: bool,
130    /// Whether to call the validate and on_load callbacks when loading the data from file
131    pub use_callbacks: bool,
132    /// Path to the file used for configuration, if the file does not exist it will be created,
133    /// the path can be absolute or relative
134    pub config_path: String,
135}
136
137/// The main manager in konfig-rust, this is intended to be created near the start of your program, and destroyed by closing it
138///
139/// `KonfigManager` allows you to register sections, which then can be loaded and saved into a single file
140///
141/// ## 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
142///
143/// example:
144/// ```
145/// use serde::{Deserialize, Serialize};
146/// use konfig_rust::*;
147/// use konfig_rust::format::*;
148///
149/// use konfig_rust_derive::KonfigSection;
150///
151/// #[derive(Serialize, Deserialize, KonfigSection)] // Aside from KonfigSection, you also have to use the Serialize and Deserialize macros
152/// struct Config {
153///     name: String,
154///     age: u32,
155/// }
156///
157/// let mut c = Config { name: "Bob".to_string(), age: 32 };
158///
159/// let mut manager = KonfigManager::new(KonfigOptions {
160///     format: Format::JSON,
161///     auto_save: true,
162///     use_callbacks: true,
163///     config_path: "config.json".to_string(),
164/// });
165///
166/// manager.register_section(&mut c).unwrap();
167///
168/// manager.load().unwrap();
169///
170/// println!("Name: {}, Age: {}", c.name, c.age); // Notice how you just access the struct like normal in memory state storage
171///
172/// manager.save().unwrap();
173/// ```
174pub struct KonfigManager {
175    opts: KonfigOptions,
176    format_handler: FormatHandlerEnum,
177    path: Box<Path>,
178    sections: HashMap<String, SectionPtr>,
179}
180
181// lazy_static! {
182//         static ref SAVE_CLOSURE: Mutex<Vec<Box< FnOnce() + Send() + 'static>>> = Mutex::new(Vec::new());
183// }
184
185impl KonfigManager {
186    /// Simply creates a new `KonfigManager`, with the passed in `KonfigOptions`
187    pub fn new(opts: KonfigOptions) -> Self {
188        let m = KonfigManager {
189            format_handler: opts.format.create_handler(),
190            path: Box::from(Path::new(&opts.config_path)),
191            opts,
192            sections: HashMap::new(),
193        };
194
195        // probably just gonna rawdog pointers here to, couse rust cries too much about it
196        if m.opts.auto_save {
197            // setup panic hook
198            // let prev_hook = panic::take_hook();
199            // panic::set_hook(Box::new(move |panic_info| {
200            //     &m.save().unwrap();
201            //     prev_hook(panic_info);
202            // }));
203
204            // TODO: setup fully later
205            // setup SIGINT and SIGTERM
206            // let mut signals = Signals::new(&[signal_hook::consts::SIGINT, signal_hook::consts::SIGTERM]).unwrap();
207
208        }
209
210        m
211    }
212
213    /// Loads the found config data from the specified file into the registered sections
214    ///
215    /// Throws: `KonfigError::LoadError`
216    pub fn load(&mut self) -> Result<(), KonfigError> {
217        if File::open(&self.path).is_err() {
218            File::create(&self.path).map_err(|err| KonfigError::LoadError(err.to_string()))?;
219        }
220        let data = read(&self.path).map_err(|err| KonfigError::LoadError(err.to_string()))?;
221
222        if data.is_empty() {
223            return Ok(());
224        }
225
226        let config: serde_json::Value = match &self.format_handler {
227            FormatHandlerEnum::JSON(handler) => handler.unmarshal(data.as_slice())?,
228            FormatHandlerEnum::YAML(handler) => handler.unmarshal(data.as_slice())?,
229            FormatHandlerEnum::TOML(handler) => handler.unmarshal(data.as_slice())?,
230        };
231
232        let config_map = config
233            .as_object()
234            .ok_or_else(|| KonfigError::LoadError("Config root must be an object".to_string()))?;
235
236        for (name, section_value) in config_map {
237            if let Some(section_ptr) = self.sections.get_mut(name) {
238                let bytes = self.format_handler.marshal(section_value)?;
239                unsafe {
240                    let section = section_ptr.as_mut();
241                    section.update_from_bytes(&bytes, &self.format_handler)?;
242                    if self.opts.use_callbacks {
243                        section.validate()?;
244                        section.on_load()?;
245                    }
246                }
247            }
248        }
249
250        Ok(())
251    }
252
253    // fn internal_save(&self) {
254    //     let mut closures = SAVE_CLOSURE.lock().unwrap();
255    //     while let Some(closure) = closures.pop() {
256    //         closure();
257    //     }
258    // }
259
260    /// Saves the registered sections to the specified file
261    ///
262    /// Throws: `KonfigError::SaveError`
263    pub fn save(&self) -> Result<(), KonfigError> {
264        let mut map: HashMap<String, serde_json::Value> = HashMap::new();
265
266        for (name, section_ptr) in &self.sections {
267            let section = unsafe { section_ptr.as_ref() };
268            let bytes = section.to_bytes(&self.format_handler)?;
269
270            let value: serde_json::Value = match &self.format_handler {
271                FormatHandlerEnum::JSON(_) => serde_json::from_slice(&bytes)
272                    .map_err(|err| KonfigError::UnmarshalError(err.to_string()))?,
273                FormatHandlerEnum::YAML(_) => serde_yaml::from_slice(&bytes)
274                    .map_err(|err| KonfigError::UnmarshalError(err.to_string()))?,
275                FormatHandlerEnum::TOML(_) => {
276                    let s = str::from_utf8(&bytes)
277                        .map_err(|err| KonfigError::UnmarshalError(err.to_string()))?;
278                    toml::from_str(s).map_err(|err| KonfigError::UnmarshalError(err.to_string()))?
279                }
280            };
281
282            map.insert(name.clone(), value);
283        }
284
285        let out = self.format_handler.marshal(&map)?;
286
287        let mut f =
288            File::create(&self.path).map_err(|err| KonfigError::SaveError(err.to_string()))?;
289
290        f.write_all(out.as_slice())
291            .map_err(|err| KonfigError::SaveError(err.to_string()))?;
292
293        Ok(())
294    }
295
296    /// Registers a new section with the KonfigManager, the section must use the `Serialize` and `Deserialize` macros
297    /// and implement the `KonfigSection` trait (or use the `#[derive(KonfigSection)]` macro to do it for you)
298    ///
299    /// Throws: `KonfigError::RegistrationError`
300    pub fn register_section<T>(&mut self, section: &mut T) -> Result<(), KonfigError>
301    where
302        T: KonfigSection + 'static,
303    {
304        let name = section.name().to_string();
305
306        if self.sections.contains_key(&name) {
307            return Err(KonfigError::RegistrationError(format!(
308                "Failed to register {}, section already registered",
309                name
310            )));
311        }
312
313        let section_ptr = SectionPtr::new(section);
314
315        self.sections.insert(name, section_ptr);
316        Ok(())
317    }
318
319    /// Quick helper that runs the validation callback on all registered sections,
320    /// useful if the config is modified a lot in memory but rarely saved or loaded
321    pub fn validate_all(&self) -> Vec<(String, Result<(), KonfigError>)> {
322        self.sections
323            .iter()
324            .map(|(name, section_ptr)| {
325                let result = unsafe { section_ptr.as_ref().validate() };
326                (name.clone(), result)
327            })
328            .collect()
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use konfig_rust_derive::KonfigSection;
336    use serde::{Deserialize, Serialize};
337
338    #[test]
339    fn test_konfig() {
340        #[derive(Serialize, Deserialize, KonfigSection)]
341        struct TestData {
342            a: i32,
343            b: String,
344        }
345
346        #[derive(Serialize, Deserialize, KonfigSection)]
347        struct TestData2 {
348            port: String,
349            host: String,
350        }
351
352        let mut t = TestData {
353            a: 1,
354            b: "test".to_string(),
355        };
356
357        let mut t2 = TestData2 {
358            port: "8080".to_string(),
359            host: "localhost".to_string(),
360        };
361
362        let mut mngr = KonfigManager::new(KonfigOptions {
363            format: Format::JSON,
364            auto_save: false,
365            use_callbacks: true,
366            config_path: "test.json".to_string(),
367        });
368
369        mngr.register_section(&mut t)
370            .map_err(|err| println!("{}", err.to_string()))
371            .unwrap();
372
373        mngr.register_section(&mut t2)
374            .map_err(|err| println!("{}", err.to_string()))
375            .unwrap();
376
377        mngr.load()
378            .map_err(|err| println!("{}", err.to_string()))
379            .unwrap();
380
381        t.a = t.a + 1;
382
383        mngr.save()
384            .map_err(|err| println!("{}", err.to_string()))
385            .unwrap();
386
387        for (name, result) in mngr.validate_all() {
388            println!("{}: {}", name, result.is_ok());
389        }
390        println!("TestData: {}, {}", t.a, t.b);
391        println!("TestData2: {}, {}", t2.port, t2.host);
392    }
393}