configory/lib.rs
1//! Batteries included application config management.
2//!
3//! Configory is a batteries included configuration management library which
4//! handles all the gory details of supporting configuration files while also
5//! supporting IPC access and overrides.
6//!
7//! # Example
8//!
9//! To get a configuration which is backed by a file that is automatically
10//! reloaded on change, you just need to create a config with [`Manager::new`]:
11//!
12//! ```rust
13//! use configory::Manager;
14//!
15//! let manager = Manager::new("configory", ()).unwrap();
16//! manager.set(&["option"], 3);
17//!
18//! assert_eq!(manager.get::<_, i32>(&["option"]), Ok(Some(3)));
19//! ```
20//!
21//! This will also automatically spawn an IPC server which allows configuration
22//! file access and modification through a socket file. If you want to disable
23//! this, you can use [`Manager::with_options`].
24//!
25//! The event handler passed to [`Manager::new`] or [`Manager::with_options`]
26//! can be used to handle configuration changes, custom IPC messages, and
27//! errors.
28//!
29//! ```rust
30//! use std::sync::Arc;
31//! use std::sync::atomic::{AtomicU8, Ordering};
32//!
33//! use configory::{Config, EventHandler, Manager};
34//!
35//! /// Event handler with a configuration change counter.
36//! struct MyEventHandler {
37//! changes: Arc<AtomicU8>,
38//! }
39//!
40//! impl EventHandler for MyEventHandler {
41//! type MessageData = ();
42//!
43//! fn ipc_changed(&self, _config: &Config) {
44//! self.changes.fetch_add(1, Ordering::Relaxed);
45//! }
46//! }
47//!
48//! // Register our event handler, which increments a counter on change.
49//! let changes = Arc::new(AtomicU8::new(0));
50//! let event_handler = MyEventHandler { changes: changes.clone() };
51//! let manager = Manager::new("configory", event_handler).unwrap();
52//!
53//! // Update the config through direct access and IPC.
54//! manager.set(&["integer"], 3);
55//! manager.ipc().unwrap().set(&["text"], "demo");
56//!
57//! // Verify the config is correct and was changed once through IPC.
58//! assert_eq!(manager.get::<_, String>(&["text"]), Ok(Some("demo".into())));
59//! assert_eq!(manager.get::<_, i32>(&["integer"]), Ok(Some(3)));
60//! assert_eq!(changes.load(Ordering::Relaxed), 1);
61//! ```
62//!
63//! # IPC client
64//!
65//! The client side of the IPC interface is constructed from the socket path,
66//! which can be acquired using [`Manager::ipc`] and [`Ipc::socket_path`].
67//!
68//! ```rust
69//! use configory::Manager;
70//! use configory::ipc::Ipc;
71//!
72//! // This would typically happen in a separate process.
73//! let manager = Manager::new("configory", ()).unwrap();
74//! let socket_path = manager.ipc().unwrap().socket_path();
75//!
76//! // Set and retrieve a configuration value through the socket.
77//! let ipc = Ipc::client(socket_path);
78//! ipc.set(&["option"], 3).unwrap();
79//! let value = ipc.get::<_, i32>(&["option"]).unwrap();
80//! assert_eq!(value, Some(3));
81//! ```
82//!
83//! # Struct Deserialization
84//!
85//! If you prefer accessing configuration values through a struct rather than
86//! using the dynamic syntax of [`Config::get`], you can deserialize the
87//! toplevel value into your struct:
88//!
89//! ```rust
90//! use configory::Manager;
91//! use serde::Deserialize;
92//!
93//! #[derive(Deserialize, Default, PartialEq, Debug)]
94//! #[serde(default)]
95//! struct MyConfig {
96//! field: String,
97//! }
98//!
99//! let manager = Manager::new("configory", ()).unwrap();
100//!
101//! // Without configuration file, the default will be empty.
102//! let my_config = manager.get::<&str, MyConfig>(&[]);
103//! assert_eq!(my_config, Ok(None));
104//!
105//! // Once changed wit the path syntax, the field will be uptaded.
106//! manager.set(&["field"], "demo");
107//! let my_config = manager.get::<&str, MyConfig>(&[]).unwrap().unwrap();
108//! assert_eq!(my_config.field, String::from("demo"));
109//! ```
110
111#![deny(missing_docs)]
112
113use std::fs;
114use std::io::{self, ErrorKind as IoErrorKind};
115use std::ops::Deref;
116use std::path::{Path, PathBuf};
117use std::sync::{Arc, RwLock};
118
119use serde::de::{Deserialize, DeserializeOwned};
120use tempfile::NamedTempFile;
121use toml::{Table, Value};
122
123use crate::ipc::{Ipc, Message};
124use crate::monitor::Watcher;
125
126#[cfg(feature = "docgen")]
127pub mod docgen;
128pub mod ipc;
129mod monitor;
130mod thread;
131
132/// Configuration manager.
133///
134/// # Example
135///
136/// ```rust
137/// use configory::Manager;
138///
139/// let manager = Manager::new("configory", ()).unwrap();
140/// manager.set(&["option"], 3);
141///
142/// assert_eq!(manager.get::<_, i32>(&["option"]), Ok(Some(3)));
143/// ```
144pub struct Manager<E> {
145 config: Config,
146 ipc: Option<Ipc>,
147
148 watcher: Option<Watcher>,
149 event_handler: Arc<E>,
150 notify: bool,
151}
152
153impl<E> Manager<E>
154where
155 E: EventHandler,
156{
157 /// Initialize the configuration manager.
158 ///
159 /// This will spawn multiple background threads and create socket files.
160 /// See [`Self::with_options`] to control the enabled features.
161 ///
162 /// # Example
163 ///
164 /// ```rust
165 /// use configory::Manager;
166 ///
167 /// let manager = Manager::new("configory", ()).unwrap();
168 /// #
169 /// # manager.set(&["option"], 3);
170 /// # assert_eq!(manager.get::<_, i32>(&["option"]), Ok(Some(3)));
171 /// ```
172 pub fn new<S: AsRef<str>>(namespace: S, event_handler: E) -> Result<Self, Error>
173 where
174 <E as EventHandler>::MessageData: DeserializeOwned,
175 {
176 let options = Options::new(namespace.as_ref()).notify(true).ipc(true);
177 Self::with_options(&options, event_handler)
178 }
179
180 /// Initialize the configuration manager with some features disabled.
181 ///
182 /// See [`Self::new`] if you want all the features available.
183 ///
184 /// # Example
185 ///
186 /// ```rust
187 /// use configory::{Manager, Options};
188 ///
189 /// // Create a config without IPC.
190 /// let options = Options::new("configory").notify(true);
191 /// let manager = Manager::with_options(&options, ()).unwrap();
192 /// #
193 /// # manager.set(&["option"], 3);
194 /// # assert_eq!(manager.get::<_, i32>(&["option"]), Ok(Some(3)));
195 /// ```
196 pub fn with_options(options: &Options, event_handler: E) -> Result<Self, Error>
197 where
198 <E as EventHandler>::MessageData: DeserializeOwned,
199 {
200 // Parse initial configuration file.
201 let mut path = dirs::config_dir()
202 .map(|dir| dir.join(options.namespace).join(format!("{}.toml", options.namespace)));
203 let config = match path.as_ref().map(|path| load_config(path)) {
204 Some(Ok(config_file)) => Config::from_config(config_file, path.clone()),
205 Some(Err(err)) => {
206 let config = Config::new(path.clone());
207 event_handler.file_error(&config, err);
208 config
209 },
210 None => Config::new(path.clone()),
211 };
212
213 let event_handler = Arc::new(event_handler);
214
215 // Spawn IPC thread.
216 let ipc = if options.ipc {
217 Some(Ipc::listen(config.clone(), options.namespace, event_handler.clone())?)
218 } else {
219 None
220 };
221
222 // Spawn file monitor.
223 let watcher = match path.take() {
224 Some(path) if options.notify => {
225 Watcher::new(config.clone(), event_handler.clone(), path)?
226 },
227 _ => None,
228 };
229
230 Ok(Self { notify: options.notify, event_handler, watcher, config, ipc })
231 }
232
233 /// Get an IPC handle.
234 ///
235 /// Will be [`None`] only if IPC was disabled using [`Self::with_options`].
236 ///
237 /// # Example
238 ///
239 /// ```rust
240 /// use configory::{Manager, Options};
241 ///
242 /// let manager = Manager::new("configory", ()).unwrap();
243 /// assert!(manager.ipc().is_some());
244 ///
245 /// let options = Options::new("configory");
246 /// let manager = Manager::with_options(&options, ()).unwrap();
247 /// assert!(manager.ipc().is_none());
248 /// ```
249 pub fn ipc(&self) -> Option<&Ipc> {
250 self.ipc.as_ref()
251 }
252
253 /// Persist the current runtime config to the config file.
254 ///
255 /// # Example
256 ///
257 /// ```rust
258 /// use configory::Manager;
259 ///
260 /// # let tempdir = tempfile::tempdir().unwrap();
261 /// # let fake_home = tempdir.path().join("configory-docs-persist");
262 /// # unsafe { std::env::set_var("XDG_CONFIG_HOME", &*fake_home.to_string_lossy()) };
263 ///
264 /// let mut manager = Manager::new("configory", ()).unwrap();
265 /// manager.set(&["option"], 3);
266 /// manager.persist().unwrap();
267 ///
268 /// # let config_path = fake_home.join("configory").join("configory.toml");
269 /// # let config = std::fs::read_to_string(&config_path).unwrap();
270 /// # assert_eq!(&config, "option = 3\n");
271 /// ```
272 pub fn persist(&mut self) -> Result<(), Error> {
273 let path = match &self.config.path {
274 Some(path) => path,
275 None => return Err(Error::NoConfigDir),
276 };
277
278 // Ensure config directory exists.
279 let parent = path.parent().unwrap();
280 fs::create_dir_all(parent)?;
281
282 // Write updated config to tempfile.
283 let tempfile = NamedTempFile::new_in(parent)?;
284 let mut values = self.config.values.write().unwrap();
285 let toml = toml::to_string_pretty(&values.merged)?;
286 fs::write(tempfile.path(), toml)?;
287
288 // Move config into place.
289 tempfile.persist(path)?;
290
291 // Clear runtime overrides, since we've just saved them.
292 values.runtime = Value::Table(Table::new());
293 values.file = values.merged.clone();
294 drop(values);
295
296 // Try to start config monitor, since this might have created the file.
297 if self.notify && self.watcher.is_none() {
298 self.watcher =
299 Watcher::new(self.config.clone(), self.event_handler.clone(), path.clone())?;
300
301 // Manually dispatch creation event.
302 self.event_handler.file_changed(&self.config);
303 }
304
305 Ok(())
306 }
307}
308
309impl<E> Deref for Manager<E> {
310 type Target = Config;
311
312 fn deref(&self) -> &Self::Target {
313 &self.config
314 }
315}
316
317/// Configuration value storage.
318#[derive(Clone)]
319pub struct Config {
320 pub(crate) values: Arc<RwLock<Values>>,
321 path: Option<PathBuf>,
322}
323
324impl Config {
325 /// Create a new empty store.
326 fn new(path: Option<PathBuf>) -> Self {
327 Self::from_config(Value::Table(Table::new()), path)
328 }
329
330 /// Create a new store from a parsed configuration file.
331 fn from_config(value: Value, path: Option<PathBuf>) -> Self {
332 Self { path, values: Arc::new(RwLock::new(Values::new(value))) }
333 }
334
335 /// Get the current value of a config option.
336 ///
337 /// This will return `Ok(None)` if the requested option does not exist, and
338 /// an error if it does not match the requested type. This will never error
339 /// if `T` is [`toml::Value`].
340 ///
341 /// # Example
342 ///
343 /// ```rust
344 /// use configory::Manager;
345 ///
346 /// let manager = Manager::new("configory", ()).unwrap();
347 /// manager.set(&["option"], 3);
348 ///
349 /// let existing_value = manager.get::<_, i32>(&["option"]);
350 /// let missing_value = manager.get::<_, i32>(&["missing"]);
351 ///
352 /// assert_eq!(existing_value, Ok(Some(3)));
353 /// assert_eq!(missing_value, Ok(None));
354 /// ```
355 pub fn get<'de, S, T>(&self, path: &[S]) -> Result<Option<T>, toml::de::Error>
356 where
357 S: AsRef<str>,
358 T: Deserialize<'de>,
359 {
360 let value = self.values.read().unwrap().get(path).cloned();
361 match value {
362 Some(value) => Ok(Some(value.try_into()?)),
363 None => Ok(None),
364 }
365 }
366
367 /// Set a runtime override for a config option.
368 ///
369 /// See [`Self::reset`] for clearing runtime overrides.
370 ///
371 /// # Example
372 ///
373 /// ```rust
374 /// use configory::Manager;
375 ///
376 /// let manager = Manager::new("configory", ()).unwrap();
377 ///
378 /// manager.set(&["option"], 3);
379 /// #
380 /// # assert_eq!(manager.get::<_, i32>(&["option"]), Ok(Some(3)));
381 /// ```
382 pub fn set<S, V>(&self, path: &[S], value: V)
383 where
384 S: AsRef<str>,
385 V: Into<Value>,
386 {
387 let value = value.into();
388 self.values.write().unwrap().set(path, value);
389 }
390
391 /// Clear the runtime override for a config option.
392 ///
393 /// See [`Self::set`] for setting runtime overrides.
394 ///
395 /// # Example
396 ///
397 /// ```rust
398 /// use configory::Manager;
399 ///
400 /// let manager = Manager::new("configory", ()).unwrap();
401 /// # manager.set(&["option"], 3);
402 ///
403 /// assert_eq!(manager.get::<_, i32>(&["option"]), Ok(Some(3)));
404 /// manager.reset(&["option"]);
405 /// assert_eq!(manager.get::<_, i32>(&["option"]), Ok(None));
406 /// ```
407 pub fn reset<S>(&self, path: &[S])
408 where
409 S: AsRef<str>,
410 {
411 self.values.write().unwrap().reset(path)
412 }
413}
414
415/// Configuration value store.
416#[derive(Debug)]
417struct Values {
418 /// Deserialized configuration file values.
419 file: Value,
420 /// Current runtime overrides, like IPC config.
421 runtime: Value,
422 /// File values merged with runtime overrides.
423 merged: Value,
424}
425
426impl Values {
427 /// Create a new config value store.
428 fn new(file: Value) -> Self {
429 let merged = file.clone();
430 Self { merged, file, runtime: Value::Table(Table::new()) }
431 }
432
433 /// Update configuration file values.
434 fn set_file(&mut self, file: Value) {
435 self.file = file;
436
437 // Apply config overrides to the file.
438 self.merged = self.file.clone();
439 toml_merge(&mut self.merged, self.runtime.clone());
440 }
441
442 /// Get the current value of a config option.
443 fn get<S>(&self, path: &[S]) -> Option<&toml::Value>
444 where
445 S: AsRef<str>,
446 {
447 toml_get(&self.merged, path)
448 }
449
450 /// Override the runtime portion of a configuration value.
451 fn set<S, V>(&mut self, path: &[S], value: V)
452 where
453 S: AsRef<str>,
454 V: Into<Value>,
455 {
456 let value = value.into();
457 toml_insert(&mut self.merged, path, value.clone());
458 toml_insert(&mut self.runtime, path, value);
459 }
460
461 /// Clear the runtime portion of a configuration value.
462 fn reset<S>(&mut self, path: &[S])
463 where
464 S: AsRef<str>,
465 {
466 // Reset merged value to configuration file if available.
467 match toml_get(&self.file, path) {
468 Some(value) => toml_insert(&mut self.merged, path, value.clone()),
469 None => toml_remove(&mut self.merged, path),
470 }
471 toml_remove(&mut self.runtime, path);
472 }
473}
474
475/// Event handler for configuration changes.
476///
477/// ```rust
478/// use std::sync::Arc;
479/// use std::sync::atomic::{AtomicU8, Ordering};
480///
481/// use configory::{Config, EventHandler, Manager};
482///
483/// /// Event handler with a configuration change counter.
484/// struct MyEventHandler {
485/// changes: Arc<AtomicU8>,
486/// }
487///
488/// impl EventHandler for MyEventHandler {
489/// type MessageData = ();
490///
491/// fn ipc_changed(&self, _config: &Config) {
492/// self.changes.fetch_add(1, Ordering::Relaxed);
493/// }
494/// }
495///
496/// // Register our event handler, which increments a counter on change.
497/// let changes = Arc::new(AtomicU8::new(0));
498/// let event_handler = MyEventHandler { changes: changes.clone() };
499/// let manager = Manager::new("configory", event_handler).unwrap();
500///
501/// // Update the config through direct access and IPC.
502/// manager.set(&["integer"], 3);
503/// manager.ipc().unwrap().set(&["text"], "demo");
504///
505/// // Verify the config is correct and was changed once through IPC.
506/// assert_eq!(manager.get::<_, String>(&["text"]), Ok(Some("demo".into())));
507/// assert_eq!(manager.get::<_, i32>(&["integer"]), Ok(Some(3)));
508/// assert_eq!(changes.load(Ordering::Relaxed), 1);
509/// ```
510pub trait EventHandler: Send + Sync + 'static {
511 /// Type for custom IPC messages.
512 type MessageData;
513
514 /// Handle configuration file changes.
515 fn file_changed(&self, _config: &Config) {}
516
517 /// Handle configuration file syntax errors.
518 fn file_error(&self, _config: &Config, _err: Error) {}
519
520 /// Handle configuration changes through IPC.
521 fn ipc_changed(&self, _config: &Config) {}
522
523 /// Handle user-defined ipc messages.
524 fn ipc_message(&self, _config: &Config, _message: Message<Self::MessageData>) {}
525}
526
527/// Dummy handler when update notifications are undesired.
528impl EventHandler for () {
529 type MessageData = ();
530}
531
532/// Configuration monitor options.
533///
534/// See [`Manager::with_options`].
535///
536/// # Example
537///
538/// ```rust
539/// use configory::Options;
540///
541/// // Disable all features.
542/// let options = Options::new("configory");
543///
544/// // Enable all features.
545/// let options = Options::new("configory").notify(true).ipc(true);
546/// ```
547pub struct Options<'a> {
548 namespace: &'a str,
549 notify: bool,
550 ipc: bool,
551}
552
553impl<'a> Options<'a> {
554 /// Create a new set of options with all features disabled.
555 ///
556 /// # Example
557 ///
558 /// ```rust
559 /// use configory::Options;
560 ///
561 /// let options = Options::new("configory");
562 /// ```
563 pub fn new(namespace: &'a str) -> Self {
564 Self { namespace, notify: false, ipc: false }
565 }
566
567 /// Enable or disable the file monitor.
568 ///
569 /// # Example
570 ///
571 /// ```rust
572 /// use configory::Options;
573 ///
574 /// let options = Options::new("configory").notify(true);
575 /// ```
576 pub fn notify(mut self, enabled: bool) -> Self {
577 self.notify = enabled;
578 self
579 }
580
581 /// Enable or disable IPC.
582 ///
583 /// # Example
584 ///
585 /// ```rust
586 /// use configory::Options;
587 ///
588 /// // Enable all features.
589 /// let options = Options::new("configory").ipc(true);
590 /// ```
591 pub fn ipc(mut self, enabled: bool) -> Self {
592 self.ipc = enabled;
593 self
594 }
595}
596
597/// Resolve a toml path key's value recursively.
598fn toml_get<'a, S>(value: &'a Value, path: &[S]) -> Option<&'a Value>
599where
600 S: AsRef<str>,
601{
602 match (value, path.first()) {
603 (Value::Table(table), Some(segment)) => {
604 let next_value = table.get(segment.as_ref())?;
605 toml_get(next_value, &path[1..])
606 },
607 (Value::Table(table), None) if table.is_empty() => None,
608 (value, _) => Some(value),
609 }
610}
611
612/// Insert a toml value, creating new tables if necessary.
613fn toml_insert<S>(value: &mut Value, path: &[S], inserting: Value)
614where
615 S: AsRef<str>,
616{
617 match (value, path.first()) {
618 (Value::Table(table), Some(segment)) => {
619 let next_value =
620 table.entry(segment.as_ref()).or_insert_with(|| Value::Table(Table::new()));
621 toml_insert(next_value, &path[1..], inserting)
622 },
623 (value, Some(segment)) => {
624 *value = Value::Table(Table::new());
625 let table = value.as_table_mut().unwrap();
626 let next_value =
627 table.entry(segment.as_ref()).or_insert_with(|| Value::Table(Table::new()));
628 toml_insert(next_value, path, inserting)
629 },
630 (value, None) => *value = inserting,
631 }
632}
633
634/// Remove a toml value.
635fn toml_remove<S>(value: &mut Value, path: &[S])
636where
637 S: AsRef<str>,
638{
639 // If the root is removed, just replace it with a new table.
640 if path.is_empty() {
641 *value = Value::Table(Table::new());
642 return;
643 }
644
645 // Values can only be removed from tables, so ignore everything else.
646 let table = match value {
647 Value::Table(table) => table,
648 _ => return,
649 };
650
651 // Remove value if it's in the current table.
652 if path.len() == 1 {
653 table.remove(path[0].as_ref());
654 }
655
656 // Recurse into the table, ignoring invalid paths.
657 if let Some(next_value) = table.get_mut(path[0].as_ref()) {
658 toml_remove(next_value, &path[1..]);
659 }
660}
661
662/// Merge two toml values together.
663fn toml_merge(base: &mut Value, new: Value) {
664 match (base, new) {
665 (Value::Table(base_table), Value::Table(new_table)) => {
666 for (key, new_value) in new_table.into_iter() {
667 match base_table.get_mut(&key) {
668 Some(base_value) => toml_merge(base_value, new_value),
669 None => _ = base_table.insert(key, new_value),
670 }
671 }
672 },
673 (Value::String(base_string), Value::String(new_string)) => *base_string = new_string,
674 (Value::Integer(base_int), Value::Integer(new_int)) => *base_int = new_int,
675 (Value::Float(base_float), Value::Float(new_float)) => *base_float = new_float,
676 (Value::Boolean(base_bool), Value::Boolean(new_bool)) => *base_bool = new_bool,
677 (Value::Datetime(base_date), Value::Datetime(new_date)) => *base_date = new_date,
678 (Value::Array(base_array), Value::Array(new_array)) => base_array.extend(new_array),
679 // On type mismatch, we just use the override.
680 (base, new) => *base = new,
681 }
682}
683
684/// Configuration errors.
685#[derive(thiserror::Error, Debug)]
686pub enum Error {
687 /// Failed to atomically replace config.
688 #[error("{0}")]
689 AtomicReplace(#[from] tempfile::PersistError),
690 /// Configuration file deserialization failed.
691 #[error("{0}")]
692 ConfigDeserialize(#[from] toml::de::Error),
693 /// Configuration file serialization failed.
694 #[error("{0}")]
695 ConfigSerialize(#[from] toml::ser::Error),
696 /// IPC message deserialization failed.
697 #[error("{0}")]
698 IpcDeserialize(#[from] serde_json::Error),
699 /// File monitor creation failed.
700 #[error("{0}")]
701 Notify(#[from] notify::Error),
702 /// IO error.
703 #[error("{0}")]
704 Io(#[from] io::Error),
705 /// Unable to resolve get system's config directory.
706 #[error("could not determine system config dir")]
707 NoConfigDir,
708}
709
710/// Deserialize a configuration file.
711pub(crate) fn load_config(path: &Path) -> Result<Value, Error> {
712 // Get file content and strip UTF-8 BOM.
713 let mut config_text = match fs::read_to_string(path) {
714 Ok(config_text) => config_text,
715 Err(err) if err.kind() == IoErrorKind::NotFound => {
716 return Ok(Value::Table(Table::new()));
717 },
718 Err(err) => return Err(err.into()),
719 };
720 if config_text.starts_with('\u{FEFF}') {
721 config_text = config_text.split_off(3);
722 }
723
724 // Deserialize configuration file.
725 let config: Value = toml::from_str(&config_text)?;
726
727 Ok(config)
728}
729
730#[cfg(test)]
731mod tests {
732 use std::env;
733
734 use serde::Deserialize;
735 use tempfile::tempdir;
736
737 use super::*;
738
739 #[test]
740 fn toml_get_simple() {
741 // Create test tree.
742 let mut table = Table::new();
743 table.insert("exists".into(), Value::Integer(3));
744 let value = Value::Table(table);
745
746 // Run tests against both implementations.
747 assert_eq!(toml_get(&value, &["exists"]), Some(&Value::Integer(3)));
748 assert_eq!(toml_get(&value, &["missing"]), None);
749 }
750
751 #[test]
752 fn toml_get_table() {
753 // Create test tree.
754 let mut nested_table = Table::new();
755 nested_table.insert("nested".into(), Value::Integer(3));
756 let mut table = Table::new();
757 table.insert("exists".into(), Value::Table(Table::new()));
758 table.insert("exists2".into(), Value::Table(nested_table));
759 let value = Value::Table(table);
760
761 // Run tests against both implementations.
762 assert_eq!(toml_get(&value, &["exists"]), None);
763 assert!(toml_get(&value, &["exists2"]).is_some());
764 assert_eq!(toml_get(&value, &["exists2", "nested"]), Some(&Value::Integer(3)));
765 }
766
767 #[test]
768 fn toml_insert_simple() {
769 // Create test tree.
770 let mut value = Value::Table(Table::new());
771
772 // Insert the new value.
773 toml_insert(&mut value, &["exists"], Value::Integer(3));
774
775 // Verify new tree structure.
776 assert_eq!(toml_get(&value, &["exists"]), Some(&Value::Integer(3)));
777 }
778
779 #[test]
780 fn toml_insert_replace() {
781 // Create test tree.
782 let mut table = Table::new();
783 table.insert("exists".into(), Value::Integer(0));
784 let mut value = Value::Table(table);
785 assert_eq!(toml_get(&value, &["exists"]), Some(&Value::Integer(0)));
786
787 // Insert the new value.
788 toml_insert(&mut value, &["exists"], Value::Integer(3));
789
790 // Verify new tree structure.
791 assert_eq!(toml_get(&value, &["exists"]), Some(&Value::Integer(3)));
792 }
793
794 #[test]
795 fn toml_insert_nested_replace() {
796 // Create test tree.
797 let mut nested_table = Table::new();
798 nested_table.insert("nested".into(), Value::Integer(0));
799 let mut table = Table::new();
800 table.insert("exists".into(), Value::Table(nested_table));
801 let mut value = Value::Table(table);
802 assert_eq!(toml_get(&value, &["exists", "nested"]), Some(&Value::Integer(0)));
803
804 // Insert the new value.
805 toml_insert(&mut value, &["exists", "nested"], Value::Integer(3));
806
807 // Verify new tree structure.
808 assert_eq!(toml_get(&value, &["exists", "nested"]), Some(&Value::Integer(3)));
809 }
810
811 #[test]
812 fn toml_insert_replace_table() {
813 // Create test tree.
814 let mut nested_table = Table::new();
815 nested_table.insert("nested".into(), Value::Integer(0));
816 nested_table.insert("nested2".into(), Value::Integer(1));
817 let mut table = Table::new();
818 table.insert("exists".into(), Value::Table(nested_table));
819 let mut value = Value::Table(table);
820 assert_eq!(toml_get(&value, &["exists", "nested"]), Some(&Value::Integer(0)));
821 assert_eq!(toml_get(&value, &["exists", "nested2"]), Some(&Value::Integer(1)));
822
823 // Insert the new value.
824 let mut new_nested_table = Table::new();
825 new_nested_table.insert("nested".into(), Value::Integer(3));
826 toml_insert(&mut value, &["exists"], Value::Table(new_nested_table));
827
828 // Verify new tree structure.
829 assert_eq!(toml_get(&value, &["exists", "nested"]), Some(&Value::Integer(3)));
830 assert_eq!(toml_get(&value, &["exists", "nested2"]), None);
831 }
832
833 #[test]
834 fn toml_insert_deep_new() {
835 // Create test tree.
836 let mut value = Value::Table(Table::new());
837
838 // Insert the new value.
839 toml_insert(&mut value, &["exists", "nested", "deep"], Value::Integer(3));
840
841 // Verify new tree structure.
842 assert_eq!(toml_get(&value, &["exists", "nested", "deep"]), Some(&Value::Integer(3)));
843 }
844
845 #[test]
846 fn toml_remove_all() {
847 let mut table = Table::new();
848 table.insert("aoeu".into(), Value::Integer(3));
849 let mut value = Value::Table(table);
850
851 toml_remove::<&str>(&mut value, &[]);
852
853 assert_eq!(value, Value::Table(Table::new()));
854 }
855
856 #[test]
857 fn toml_remove_simple() {
858 let mut table = Table::new();
859 table.insert("aoeu".into(), Value::Integer(3));
860 table.insert("bbb".into(), Value::Integer(9));
861 let mut value = Value::Table(table);
862
863 toml_remove(&mut value, &["bbb"]);
864
865 let mut expected_table = Table::new();
866 expected_table.insert("aoeu".into(), Value::Integer(3));
867 let expected = Value::Table(expected_table);
868
869 assert_eq!(value, expected);
870 }
871
872 #[test]
873 fn toml_merge_tables() {
874 let mut table_a = Table::new();
875 table_a.insert("yyy".into(), Value::Integer(1));
876 table_a.insert("aoeu".into(), Value::Integer(3));
877 let mut a = Value::Table(table_a);
878
879 let mut table_b = Table::new();
880 table_b.insert("aoeu".into(), Value::Integer(0));
881 table_b.insert("xxx".into(), Value::Integer(9));
882 let b = Value::Table(table_b);
883
884 toml_merge(&mut a, b);
885
886 let mut expected_table = Table::new();
887 expected_table.insert("yyy".into(), Value::Integer(1));
888 expected_table.insert("aoeu".into(), Value::Integer(0));
889 expected_table.insert("xxx".into(), Value::Integer(9));
890 let expected = Value::Table(expected_table);
891
892 assert_eq!(a, expected);
893 }
894
895 #[test]
896 fn toml_merge_array() {
897 let mut table_a = Table::new();
898 table_a.insert("a".into(), Value::Array(vec![Value::Integer(3)]));
899 let mut a = Value::Table(table_a);
900
901 let mut table_b = Table::new();
902 table_b.insert("a".into(), Value::Array(vec![Value::Integer(9)]));
903 let b = Value::Table(table_b);
904
905 toml_merge(&mut a, b);
906
907 let mut expected_table = Table::new();
908 expected_table.insert("a".into(), Value::Array(vec![Value::Integer(3), Value::Integer(9)]));
909 let expected = Value::Table(expected_table);
910
911 assert_eq!(a, expected);
912 }
913
914 #[test]
915 fn toml_merge_mismatched_types() {
916 let mut table_a = Table::new();
917 table_a.insert("a".into(), Value::Integer(0));
918 let mut a = Value::Table(table_a);
919
920 let mut table_b = Table::new();
921 table_b.insert("a".into(), Value::String("test".into()));
922 let b = Value::Table(table_b);
923
924 toml_merge(&mut a, b);
925
926 let mut expected_table = Table::new();
927 expected_table.insert("a".into(), Value::String("test".into()));
928 let expected = Value::Table(expected_table);
929
930 assert_eq!(a, expected);
931 }
932
933 #[test]
934 fn config_get_merged() {
935 let test_id = "configory_config_get_merged";
936
937 #[derive(Deserialize, PartialEq, Debug)]
938 struct Test {
939 integer: i32,
940 text: String,
941 }
942
943 // Create a temporary config with an initial value present.
944 let tempdir = tempdir().unwrap();
945 let fake_home = tempdir.path().join(test_id);
946 unsafe { env::set_var("XDG_CONFIG_HOME", &*fake_home.to_string_lossy()) };
947 let config_path = fake_home.join(test_id).join(format!("{test_id}.toml"));
948 fs::create_dir_all(config_path.parent().unwrap()).unwrap();
949 fs::write(&config_path, "integer = 13").unwrap();
950
951 // Load config and add a runtime option.
952 let manager = Manager::new(test_id, ()).unwrap();
953 manager.set(&["text"], "test");
954
955 // Ensure runtime and file values are merged in the root table.
956 let root = manager.get::<&str, Test>(&[]);
957 assert_eq!(root, Ok(Some(Test { integer: 13, text: "test".into() })));
958 }
959}