1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2#![deny(missing_docs)]
3
4use std::{
8 borrow::Cow,
9 collections::{BTreeMap, HashMap},
10 path::{Path, PathBuf},
11 str::FromStr,
12};
13
14use serde::{de::DeserializeOwned, Serialize};
15use thiserror::Error;
16use toml::Value;
17
18pub type Result<T> = std::result::Result<T, Error>;
20
21pub const DEFAULT_DOTENV_PATH: &str = ".env.toml";
23
24pub const DEFAULT_CONFIG_VARIABLE_NAME: &str = "CONFIG";
27
28pub const DEFAULT_MAP_ENV_DIVIDER: &str = "__";
31
32#[derive(Debug, Clone)]
34pub enum ConfigSource {
35 Merged {
37 from: Box<Self>,
39 into: Box<Self>,
41 },
42 DotEnv(PathBuf),
44 File(PathBuf),
46 Environment {
48 variable_names: Vec<String>,
50 },
51}
52
53impl std::fmt::Display for ConfigSource {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 match self {
56 ConfigSource::Merged { from, into } => write!(f, "({from}) merged into ({into})"),
57 ConfigSource::DotEnv(path) => write!(f, "dotenv TOML file {path:?}"),
58 ConfigSource::File(path) => write!(f, "config TOML file {path:?}"),
59 ConfigSource::Environment { variable_names } => {
60 let variable_names = variable_names.join(", ");
61 write!(f, "environment variables {variable_names}")
62 }
63 }
64 }
65}
66
67#[derive(Debug, Error)]
69#[error(transparent)]
70pub struct Error(#[from] InnerError);
71
72#[derive(Debug, Error)]
74#[non_exhaustive]
75enum InnerError {
76 #[error("Error reading {name} environment variable")]
78 ErrorReadingEnvironmentVariable {
79 name: String,
81 #[source]
83 error: std::env::VarError,
84 },
85 #[error("Error reading TOML file {path:?}")]
87 ErrorReadingFile {
88 path: PathBuf,
90 #[source]
92 error: std::io::Error,
93 },
94 #[error("Error parsing TOML file {path:?}")]
96 ErrorParsingTomlFile {
97 path: PathBuf,
99 #[source]
101 error: Box<toml::de::Error>,
102 },
103 #[error("Cannot parse {key} as environment variable in {path:?}. Advice: {advice}")]
105 CannotParseTomlDotEnvFile {
106 key: String,
108 path: PathBuf,
110 advice: String,
112 },
113 #[error("Error parsing config key ({name}) in TOML config file {path:?}")]
115 ErrorParsingTomlDotEnvFileKey {
116 name: String,
118 path: PathBuf,
120 #[source]
122 error: Box<toml::de::Error>,
123 },
124 #[error(
127 "Error parsing config environment variable ({name}={value:?}) as the config or if it is a filename, the file does not exist."
128 )]
129 ErrorParsingEnvironmentVariableAsConfigOrFile {
130 name: String,
132 value: String,
134 #[source]
136 error: Box<toml::de::Error>,
137 },
138 #[error(
140 "Error parsing the {path:?} as `.env.toml` format file:\n{value:#?}\nTop level should be a table."
141 )]
142 UnexpectedTomlDotEnvFileFormat {
143 path: PathBuf,
145 value: Value,
147 },
148 #[error("Error parsing merged configuration from {source}")]
150 ErrorParsingMergedToml {
151 source: ConfigSource,
153 #[source]
155 error: Box<toml::de::Error>,
156 },
157 #[error("Error merging configuration {from} into {into}: {error}")]
159 ErrorMerging {
160 from: ConfigSource,
162 into: ConfigSource,
164 error: serde_toml_merge::Error,
166 },
167 #[error("Error inserting toml value")]
168 InsertTomlValueError(#[from] InsertTomlValueError),
169}
170
171#[derive(Default, Clone, Copy)]
173pub enum Logging {
174 #[default]
176 None,
177 StdOut,
181 #[cfg(feature = "log")]
183 Log,
184}
185
186type InnerResult<T> = std::result::Result<T, InnerError>;
187
188#[derive(Debug, Clone, Default)]
193pub struct TomlKeyPath(Vec<PathElement>);
194
195#[derive(Debug, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)]
196enum PathElement {
197 TableProperty(String),
198 ArrayIndex(usize),
199}
200
201impl std::fmt::Display for PathElement {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 match self {
204 PathElement::TableProperty(p) => p.fmt(f),
205 PathElement::ArrayIndex(i) => i.fmt(f),
206 }
207 }
208}
209
210impl TomlKeyPath {
211 pub fn resolve<'a>(&self, value: &'a toml::Value) -> Option<&'a toml::Value> {
248 Self::resolve_impl(&mut self.clone(), value)
249 }
250
251 fn resolve_impl<'a>(key: &mut Self, value: &'a toml::Value) -> Option<&'a toml::Value> {
252 if key.0.is_empty() {
253 return Some(value);
254 }
255
256 let current_key = key.0.remove(0);
257
258 match value {
259 Value::Table(table) => match current_key {
260 PathElement::TableProperty(p) => {
261 let value = table.get(&p)?;
262 Self::resolve_impl(key, value)
263 }
264 PathElement::ArrayIndex(_) => None,
265 },
266 Value::Array(array) => match current_key {
267 PathElement::ArrayIndex(i) => {
268 let value = array.get(i)?;
269 Self::resolve_impl(key, value)
270 }
271 PathElement::TableProperty(_) => None,
272 },
273 _ => None,
274 }
275 }
276}
277
278impl std::fmt::Display for TomlKeyPath {
279 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280 f.write_str(
281 &self
282 .0
283 .iter()
284 .map(|e| e.to_string())
285 .collect::<Vec<_>>()
286 .join("."),
287 )
288 }
289}
290
291impl FromStr for TomlKeyPath {
292 type Err = ();
293
294 fn from_str(s: &str) -> std::result::Result<Self, ()> {
313 if s.is_empty() {
314 return Ok(Self::default());
315 }
316
317 let v: Vec<PathElement> = s
318 .split('.')
319 .filter_map(|k| {
320 if k.is_empty() {
321 None
322 } else {
323 if let Ok(i) = usize::from_str(k) {
324 Some(PathElement::ArrayIndex(i))
325 } else {
326 Some(PathElement::TableProperty(k.to_owned()))
327 }
328 }
329 })
330 .collect();
331
332 Ok(Self(v))
333 }
334}
335
336pub struct AutoMapEnvArgs<'a> {
338 pub divider: &'a str,
341 pub prefix: Option<&'a str>,
343 pub transform: Box<dyn Fn(&str) -> String>,
346}
347
348impl Default for AutoMapEnvArgs<'_> {
349 fn default() -> Self {
350 Self {
351 divider: DEFAULT_MAP_ENV_DIVIDER,
352 prefix: None,
353 transform: Box::new(|name| name.to_lowercase()),
354 }
355 }
356}
357
358pub struct Args<'a> {
360 pub dotenv_path: &'a Path,
362 pub config_path: Option<&'a Path>,
364 pub config_variable_name: &'a str,
366 pub logging: Logging,
368 pub map_env: HashMap<&'a str, TomlKeyPath>,
370 pub auto_map_env: Option<AutoMapEnvArgs<'a>>,
372}
373
374impl Default for Args<'static> {
375 fn default() -> Self {
376 Self {
377 dotenv_path: Path::new(DEFAULT_DOTENV_PATH),
378 config_path: None,
379 config_variable_name: DEFAULT_CONFIG_VARIABLE_NAME,
380 logging: Logging::default(),
381 map_env: HashMap::default(),
382 auto_map_env: None,
383 }
384 }
385}
386
387fn log_info(logging: Logging, args: std::fmt::Arguments<'_>) {
388 match logging {
389 Logging::None => {}
390 Logging::StdOut => println!("INFO {}: {}", module_path!(), std::fmt::format(args)),
391 #[cfg(feature = "log")]
392 Logging::Log => log::info!("{}", std::fmt::format(args)),
393 }
394}
395
396fn initialize_dotenv_toml<'a, C: DeserializeOwned + Serialize>(
399 dotenv_path: &'a Path,
400 config_variable_name: &'a str,
401 logging: Logging,
402) -> InnerResult<Option<C>> {
403 let path = Path::new(dotenv_path);
404 if !path.exists() {
405 return Ok(None);
406 }
407
408 log_info(
409 logging,
410 format_args!("Loading config and environment variables from dotenv {path:?}"),
411 );
412
413 let env_str = std::fs::read_to_string(path).map_err(|error| InnerError::ErrorReadingFile {
414 path: path.to_owned(),
415 error,
416 })?;
417 let env: Value =
418 toml::from_str(&env_str).map_err(|error| InnerError::ErrorParsingTomlFile {
419 path: path.to_owned(),
420 error: error.into(),
421 })?;
422 let table: toml::value::Table = match env {
423 Value::Table(table) => table,
424 unexpected => {
425 return Err(InnerError::UnexpectedTomlDotEnvFileFormat {
426 path: path.to_owned(),
427 value: unexpected,
428 });
429 }
430 };
431
432 if table.is_empty() {
433 return Ok(None);
434 }
435
436 let mut config: Option<C> = None;
437 let mut set_keys: String = String::new();
438 for (key, value) in table {
439 let value_string = match value {
440 Value::Table(_) => {
441 if key.as_str() != config_variable_name {
442 return Err(InnerError::CannotParseTomlDotEnvFile {
443 key,
444 path: path.to_owned(),
445 advice: format!("Only a table with {config_variable_name} is allowed in a .toml.env format file."),
446 });
447 }
448 match C::deserialize(value.clone()) {
449 Ok(c) => config = Some(c),
450 Err(error) => {
451 return Err(InnerError::ErrorParsingTomlDotEnvFileKey {
452 name: key,
453 path: path.to_owned(),
454 error: error.into(),
455 })
456 }
457 }
458 None
459 }
460 Value::String(value) => Some(value),
461 Value::Integer(value) => Some(value.to_string()),
462 Value::Float(value) => Some(value.to_string()),
463 Value::Boolean(value) => Some(value.to_string()),
464 Value::Datetime(value) => Some(value.to_string()),
465 Value::Array(value) => {
466 return Err(InnerError::CannotParseTomlDotEnvFile {
467 key,
468 path: path.to_owned(),
469 advice: format!("Array values are not supported: {value:?}"),
470 })
471 }
472 };
473
474 if let Some(value_string) = value_string {
475 set_keys.push('\n');
476 set_keys.push_str(key.as_str());
477 std::env::set_var(key.as_str(), value_string)
478 }
479 }
480
481 log_info(
482 logging,
483 format_args!(
484 "Set environment variables specified in {dotenv_path:?}:\x1b[34m{set_keys}\x1b[0m"
485 ),
486 );
487 Ok(config)
488}
489
490#[derive(Debug, thiserror::Error)]
491enum InsertTomlValueError {
492 #[error("Table property {property:?} can only be used to index into a table. Cannot index into {value:?}")]
493 TablePropertyCannotIndex {
494 property: String,
495 value: toml::Value,
496 },
497 #[error(
498 "Array index {index} can only be used to index into an array. Cannot index into {value:?}"
499 )]
500 ArrayIndexCannotIndex { index: usize, value: toml::Value },
501 #[error("Array index {index} cannot be greater than the length of {array:?}")]
502 ArrayOutOfBounds {
503 index: usize,
504 array: Vec<toml::Value>,
505 },
506}
507
508fn insert_toml_value(
512 value: &mut toml::Value,
513 mut path: TomlKeyPath,
514 new_value: Value,
515) -> std::result::Result<(), InsertTomlValueError> {
516 if path.0.is_empty() {
517 *value = new_value;
518 return Ok(());
519 }
520
521 let current_key = path.0.remove(0);
522 let next_key = path.0.get(0);
523
524 match (current_key, value) {
525 (PathElement::TableProperty(property), Value::Table(table)) => {
526 let next_value = table.get_mut(&property);
527 match (next_value, next_key) {
528 (None, None) => {
529 table.insert(property, new_value);
530 return Ok(());
531 }
532 (None, Some(PathElement::ArrayIndex(_))) => {
533 table.insert(property.clone(), toml::Value::Array(Vec::with_capacity(1)));
534 return insert_toml_value(
535 table
536 .get_mut(&property)
537 .expect("Expect inserted property to be present"),
538 path,
539 new_value,
540 );
541 }
542 (None, Some(PathElement::TableProperty(_))) => {
543 table.insert(
544 property.clone(),
545 toml::Value::Table(toml::Table::with_capacity(1)),
546 );
547 return insert_toml_value(
548 table
549 .get_mut(&property)
550 .expect("Expect inserted property to be present"),
551 path,
552 new_value,
553 );
554 }
555 (Some(next_value), None) => {
556 *next_value = new_value;
557 return Ok(());
558 }
559 (Some(next_value), Some(_)) => {
560 return insert_toml_value(next_value, path, new_value)
561 }
562 }
563 }
564 (PathElement::TableProperty(property), value) => {
565 return Err(InsertTomlValueError::TablePropertyCannotIndex {
566 property,
567 value: value.clone(),
568 })
569 }
570 (PathElement::ArrayIndex(index), Value::Array(array)) => {
571 if index > array.len() {
572 return Err(InsertTomlValueError::ArrayOutOfBounds {
573 index,
574 array: array.clone(),
575 });
576 }
577 let next_value = array.get_mut(index);
578 match (next_value, next_key) {
579 (None, None) => {
580 array.insert(index, new_value);
581 return Ok(());
582 }
583 (None, Some(PathElement::ArrayIndex(_))) => {
584 array.insert(index, toml::Value::Array(Vec::with_capacity(1)));
585 return insert_toml_value(
586 array
587 .get_mut(index)
588 .expect("Expect inserted element to be present"),
589 path,
590 new_value,
591 );
592 }
593 (None, Some(PathElement::TableProperty(_))) => {
594 array.insert(index, toml::Value::Table(toml::Table::with_capacity(1)));
595 return insert_toml_value(
596 array
597 .get_mut(index)
598 .expect("Expect inserted element to be present"),
599 path,
600 new_value,
601 );
602 }
603 (Some(next_value), None) => {
604 *next_value = new_value;
605 return Ok(());
606 }
607 (Some(next_value), Some(_)) => {
608 return insert_toml_value(next_value, path, new_value)
609 }
610 }
611 }
612 (PathElement::ArrayIndex(index), value) => {
613 Err(InsertTomlValueError::ArrayIndexCannotIndex {
614 index,
615 value: value.clone(),
616 })
617 }
618 }
619}
620
621fn initialize_env(
623 logging: Logging,
624 map_env: HashMap<&'_ str, TomlKeyPath>,
625 auto_args: Option<AutoMapEnvArgs<'_>>,
626 config_variable_name: &'_ str,
627) -> InnerResult<Option<Value>> {
628 fn parse_toml_value(value: String) -> Value {
629 if let Ok(value) = bool::from_str(&value) {
630 return Value::Boolean(value);
631 }
632 if let Ok(value) = f64::from_str(&value) {
633 return Value::Float(value);
634 }
635 if let Ok(value) = i64::from_str(&value) {
636 return Value::Integer(value);
637 }
638 if let Ok(value) = toml::value::Datetime::from_str(&value) {
639 return Value::Datetime(value);
640 }
641
642 Value::String(value)
643 }
644
645 let mut map_env: BTreeMap<Cow<'_, str>, TomlKeyPath> = map_env
648 .into_iter()
649 .map(|(key, value)| (Cow::Borrowed(key), value))
650 .collect();
651
652 if let Some(auto_args) = auto_args {
653 let mut prefix = auto_args.prefix.unwrap_or(config_variable_name).to_owned();
654 prefix.push_str(auto_args.divider);
655 for (key, _) in std::env::vars_os() {
656 let key = if let Some(key) = key.to_str() {
657 key.to_owned()
658 } else {
659 continue;
660 };
661
662 let key_without_prefix: &str = if let Some(0) = key.find(&prefix) {
663 key.split_at(prefix.len()).1
664 } else {
665 continue;
666 };
667
668 let key_transformed = (auto_args.transform)(key_without_prefix);
669 let toml_key: TomlKeyPath =
670 if let Ok(key) = key_transformed.replace(auto_args.divider, ".").parse() {
671 key
672 } else {
673 continue;
674 };
675
676 map_env.entry(key.into()).or_insert(toml_key);
677 }
678 }
679
680 if map_env.is_empty() {
681 return Ok(None);
682 }
683
684 if !matches!(logging, Logging::None) {
685 let mut buffer = String::new();
686 buffer.push_str("\x1b[34m");
687 for (k, v) in &map_env {
688 if std::env::var(k.as_ref()).is_ok() {
689 buffer.push_str(&format!("\n{k} => {v}"));
690 }
691 }
692 buffer.push_str("\x1b[0m");
693 log_info(
694 logging,
695 format_args!("Loading config from current environment variables: {buffer}"),
696 );
697 }
698
699 log_info(logging, format_args!("Loading config from environment"));
700
701 let mut config = toml::Value::Table(toml::Table::new());
702 for (variable_name, toml_key) in map_env {
703 let value = match std::env::var(variable_name.as_ref()) {
704 Ok(value) => value,
705 Err(std::env::VarError::NotPresent) => continue,
706 Err(error) => {
707 return Err(InnerError::ErrorReadingEnvironmentVariable {
708 name: (*variable_name.into_owned()).to_owned(),
709 error,
710 })
711 }
712 };
713 let value = parse_toml_value(value);
714 insert_toml_value(&mut config, toml_key.clone(), value)?;
715 }
716
717 Ok(Some(config.into()))
718}
719
720pub fn initialize<C>(args: Args<'_>) -> Result<Option<C>>
726where
727 C: DeserializeOwned + Serialize,
728{
729 let config_variable_name = args.config_variable_name;
730 let logging = args.logging;
731 let dotenv_path = args.dotenv_path;
732
733 let config_env_config: Option<(Value, ConfigSource)> = match std::env::var(config_variable_name) {
734 Ok(variable_value) => match toml::from_str(&variable_value) {
735 Ok(config) => {
736 log_info(
737 logging,
738 format_args!(
739 "Options loaded from `{config_variable_name}` environment variable"
740 ),
741 );
742 Ok(Some(config))
743 }
744 Err(error) => {
745 let path = Path::new(&variable_value);
746 if path.is_file() {
747 log_info(
748 args.logging,
749 format_args!("Loading environment variables from {path:?}"),
750 );
751
752 let config_str =
753 std::fs::read_to_string(path).map_err(|error| InnerError::ErrorReadingFile {
754 path: path.to_owned(),
755 error,
756 })?;
757 let config: Value = toml::from_str(&config_str).map_err(|error| {
758 InnerError::ErrorParsingTomlFile {
759 path: path.to_owned(),
760 error: error.into(),
761 }
762 })?;
763 log_info(logging, format_args!("Options loaded from file specified in `{config_variable_name}` environment variable: {path:?}"));
764 Ok(Some(config))
765 } else {
766 Err(InnerError::ErrorParsingEnvironmentVariableAsConfigOrFile {
767 name: config_variable_name.to_owned(),
768 value: variable_value,
769 error: error.into(),
770 })
771 }
772 }
773 },
774 Err(std::env::VarError::NotPresent) => {
775 log_info(
776 logging,
777 format_args!(
778 "No environment variable with the name {config_variable_name} found, using default options."
779 ),
780 );
781 Ok(None)
782 }
783 Err(error) => Err(InnerError::ErrorReadingEnvironmentVariable {
784 name: config_variable_name.to_owned(),
785 error,
786 }),
787 }?.map(|config| {
788 let source = ConfigSource::DotEnv(args.dotenv_path.to_owned());
789 (config, source)
790 });
791
792 let dotenv_config =
793 initialize_dotenv_toml(dotenv_path, config_variable_name, logging)?.map(|config| {
794 (
795 config,
796 ConfigSource::Environment {
797 variable_names: vec![args.config_variable_name.to_owned()],
798 },
799 )
800 });
801
802 let config: Option<(Value, ConfigSource)> = match (dotenv_config, config_env_config) {
803 (None, None) => None,
804 (None, Some(config)) => Some(config),
805 (Some(config), None) => Some(config),
806 (Some(from), Some(into)) => {
807 let config = serde_toml_merge::merge(into.0, from.0).map_err(|error| {
808 InnerError::ErrorMerging {
809 from: from.1.clone(),
810 into: into.1.clone(),
811 error,
812 }
813 })?;
814
815 let source = ConfigSource::Merged {
816 from: from.1.into(),
817 into: into.1.into(),
818 };
819
820 Some((config, source))
821 }
822 };
823
824 let env_config = initialize_env(
825 args.logging,
826 args.map_env.clone(),
827 args.auto_map_env,
828 config_variable_name,
829 )?
830 .map(|value| {
831 (
832 value,
833 ConfigSource::Environment {
834 variable_names: args.map_env.keys().map(|key| (*key).to_owned()).collect(),
835 },
836 )
837 });
838
839 let config = match (config, env_config) {
840 (None, None) => None,
841 (None, Some(config)) => Some(config),
842 (Some(config), None) => Some(config),
843 (Some(from), Some(into)) => {
844 let config = serde_toml_merge::merge(into.0, from.0).map_err(|error| {
845 InnerError::ErrorMerging {
846 from: from.1.clone(),
847 into: into.1.clone(),
848 error,
849 }
850 })?;
851
852 let source = ConfigSource::Merged {
853 from: from.1.into(),
854 into: into.1.into(),
855 };
856 Some((config, source))
857 }
858 };
859
860 let file_config: Option<(Value, ConfigSource)> =
861 Option::transpose(args.config_path.map(|path| {
862 if path.is_file() {
863 let file_string = std::fs::read_to_string(path).map_err(|error| {
864 InnerError::ErrorReadingFile {
865 path: path.to_owned(),
866 error,
867 }
868 })?;
869 return Result::Ok(Some((
870 toml::from_str(&file_string).map_err(|error| {
871 InnerError::ErrorParsingTomlFile {
872 path: path.to_owned(),
873 error: error.into(),
874 }
875 })?,
876 ConfigSource::File(path.to_owned()),
877 )));
878 }
879 Ok(None)
880 }))?
881 .flatten();
882
883 let config = match (config, file_config) {
884 (None, None) => None,
885 (None, Some(config)) => Some(config),
886 (Some(config), None) => Some(config),
887 (Some(from), Some(into)) => {
888 let config = serde_toml_merge::merge(into.0, from.0).map_err(|error| {
889 InnerError::ErrorMerging {
890 from: from.1.clone(),
891 into: into.1.clone(),
892 error,
893 }
894 })?;
895
896 let source = ConfigSource::Merged {
897 from: from.1.into(),
898 into: into.1.into(),
899 };
900 Some((config, source))
901 }
902 };
903
904 let config = Option::transpose(config.map(|(config, source)| {
905 C::deserialize(config).map_err(|error| InnerError::ErrorParsingMergedToml {
906 source,
907 error: error.into(),
908 })
909 }))?;
910
911 match (logging, config.as_ref()) {
912 (_, Some(config)) => {
913 let config_string = toml::to_string_pretty(&config)
914 .expect("Expected to be able to re-serialize config toml");
915 log_info(
916 logging,
917 format_args!("Parsed configuration:\n\x1b[34m{config_string}\x1b[0m"),
918 );
919 }
920 (Logging::None, _) | (_, None) => {}
921 }
922
923 Ok(config)
924}
925
926#[cfg(test)]
927mod test {
928 use crate::InsertTomlValueError;
929
930 use super::insert_toml_value;
931 #[test]
932 fn insert_toml_value_empty_path() {
933 let mut value = toml::Value::String("Hello".to_owned());
934 insert_toml_value(
935 &mut value,
936 "".parse().unwrap(),
937 toml::Value::String("World".to_owned()),
938 )
939 .unwrap();
940 assert_eq!(value.as_str().unwrap(), "World");
941 }
942
943 #[test]
944 fn insert_toml_value_table_property() {
945 let mut value = toml::Value::Table(toml::Table::new());
946 insert_toml_value(
947 &mut value,
948 "child".parse().unwrap(),
949 toml::Value::String("Hello Child".to_owned()),
950 )
951 .unwrap();
952 assert_eq!(value.get("child").unwrap().as_str().unwrap(), "Hello Child");
953 }
954
955 #[test]
956 fn insert_toml_value_table_property_property() {
957 let mut value = toml::Value::Table(toml::Table::new());
958 insert_toml_value(
959 &mut value,
960 "child.value".parse().unwrap(),
961 toml::Value::String("Hello Child Value".to_owned()),
962 )
963 .unwrap();
964 assert_eq!(
965 value
966 .get("child")
967 .unwrap()
968 .get("value")
969 .unwrap()
970 .as_str()
971 .unwrap(),
972 "Hello Child Value"
973 );
974 }
975
976 #[test]
977 fn insert_toml_value_create_array() {
978 let mut value = toml::Value::Array(Vec::new());
979 insert_toml_value(
980 &mut value,
981 "0".parse().unwrap(),
982 toml::Value::String("Hello Element".to_owned()),
983 )
984 .unwrap();
985 assert_eq!(value.get(0).unwrap().as_str().unwrap(), "Hello Element");
986 }
987
988 #[test]
989 fn insert_toml_value_array_out_of_bounds_error() {
990 let mut value = toml::Value::Array(Vec::new());
991 let error = insert_toml_value(
992 &mut value,
993 "1".parse().unwrap(),
994 toml::Value::String("Hello Element".to_owned()),
995 )
996 .unwrap_err();
997
998 assert!(matches!(
999 error,
1000 InsertTomlValueError::ArrayOutOfBounds { .. }
1001 ))
1002 }
1003
1004 #[test]
1005 fn insert_toml_value_table_property_index_error() {
1006 let mut value = toml::Value::Array(Vec::new());
1007 let error = insert_toml_value(
1008 &mut value,
1009 "key".parse().unwrap(),
1010 toml::Value::String("Hello Element".to_owned()),
1011 )
1012 .unwrap_err();
1013
1014 assert!(matches!(
1015 error,
1016 InsertTomlValueError::TablePropertyCannotIndex { .. }
1017 ))
1018 }
1019
1020 #[test]
1021 fn insert_toml_value_array_index_cannot_index_error() {
1022 let mut value = toml::Value::Table(toml::Table::new());
1023 let error = insert_toml_value(
1024 &mut value,
1025 "0".parse().unwrap(),
1026 toml::Value::String("Hello Element".to_owned()),
1027 )
1028 .unwrap_err();
1029
1030 assert!(matches!(
1031 error,
1032 InsertTomlValueError::ArrayIndexCannotIndex { .. }
1033 ))
1034 }
1035
1036 #[test]
1037 fn insert_toml_value_table_child_create_array() {
1038 let mut value = toml::Value::Table(toml::Table::new());
1039 insert_toml_value(
1040 &mut value,
1041 "child.0".parse().unwrap(),
1042 toml::Value::String("Hello Element".to_owned()),
1043 )
1044 .unwrap();
1045 assert_eq!(
1046 value
1047 .get("child")
1048 .unwrap()
1049 .get(0)
1050 .unwrap()
1051 .as_str()
1052 .unwrap(),
1053 "Hello Element"
1054 );
1055 }
1056}