mod env;
pub mod error;
use std::{
fs,
path::{Path, PathBuf},
};
use error::DotCfgError;
use serde::{Deserialize, Serialize};
#[cfg(not(any(feature = "toml", feature = "json", feature = "yaml")))]
compile_error!(
"dotcfg requires at least one of the `toml`, `json` or `yaml` features to be enabled"
);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DirStrategy {
Dot,
Xdg,
Custom(PathBuf),
}
pub enum Format {
#[cfg(feature = "toml")]
Toml,
#[cfg(feature = "json")]
Json,
#[cfg(feature = "yaml")]
Yaml,
}
pub struct DotCfg {
app_name: String,
strategy: DirStrategy,
format: Format,
filename: String,
env_prefix: Option<String>,
}
impl DotCfg {
pub fn new(app_name: impl Into<String>) -> Self {
Self {
app_name: app_name.into(),
strategy: DirStrategy::Dot,
#[cfg(feature = "toml")]
format: Format::Toml,
#[cfg(all(feature = "json", not(feature = "toml")))]
format: Format::Json,
#[cfg(all(feature = "yaml", not(feature = "toml"), not(feature = "json")))]
format: Format::Yaml,
filename: "config".to_string(),
env_prefix: None,
}
}
pub fn xdg(mut self) -> Self {
self.strategy = DirStrategy::Xdg;
self
}
pub fn dot(mut self) -> Self {
self.strategy = DirStrategy::Dot;
self
}
pub fn at_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.strategy = DirStrategy::Custom(path.into());
self
}
pub fn find_in_ancestors(self) -> Result<Option<Self>, DotCfgError> {
let cwd = std::env::current_dir().map_err(DotCfgError::Io)?;
self.find_in_ancestors_from(cwd)
}
pub fn find_in_ancestors_from(
self,
start: impl AsRef<Path>,
) -> Result<Option<Self>, DotCfgError> {
let ext = match &self.format {
#[cfg(feature = "toml")]
Format::Toml => "toml",
#[cfg(feature = "json")]
Format::Json => "json",
#[cfg(feature = "yaml")]
Format::Yaml => "yaml",
};
let file_name = format!("{}.{}", self.filename, ext);
let dot_dir = format!(".{}", self.app_name);
for ancestor in start.as_ref().ancestors() {
let dir = ancestor.join(&dot_dir);
let file = dir.join(&file_name);
if file.is_file() {
return Ok(Some(Self {
app_name: self.app_name,
strategy: DirStrategy::Custom(dir),
format: self.format,
filename: self.filename,
env_prefix: self.env_prefix,
}));
}
}
Ok(None)
}
#[cfg(feature = "json")]
pub fn json(mut self) -> Self {
self.format = Format::Json;
self
}
#[cfg(feature = "yaml")]
pub fn yaml(mut self) -> Self {
self.format = Format::Yaml;
self
}
#[cfg(feature = "toml")]
pub fn toml(mut self) -> Self {
self.format = Format::Toml;
self
}
pub fn filename(mut self, name: impl Into<String>) -> Self {
self.filename = name.into();
self
}
pub fn with_env_prefix(mut self, prefix: impl Into<String>) -> Self {
self.env_prefix = Some(prefix.into());
self
}
fn env_var_name(&self, key: &str) -> Option<String> {
let prefix = self.env_prefix.as_ref()?;
Some(format!(
"{}_{}",
prefix.to_ascii_uppercase(),
key.replace('.', "_").to_ascii_uppercase()
))
}
fn env_override(&self, key: &str) -> Result<Option<(String, String)>, DotCfgError> {
let Some(var) = self.env_var_name(key) else {
return Ok(None);
};
match std::env::var(&var) {
Ok(raw) => Ok(Some((var, raw))),
Err(std::env::VarError::NotPresent) => Ok(None),
Err(std::env::VarError::NotUnicode(_)) => Err(DotCfgError::EnvNotUnicode(var)),
}
}
pub fn dir(&self) -> Result<PathBuf, DotCfgError> {
let dir = match &self.strategy {
DirStrategy::Dot => {
let home = home::home_dir().ok_or(DotCfgError::NoHomeDir)?;
home.join(format!(".{}", self.app_name))
}
DirStrategy::Xdg => {
use etcetera::app_strategy::{AppStrategy, AppStrategyArgs, Xdg};
let strategy = Xdg::new(AppStrategyArgs {
top_level_domain: "".to_string(),
author: "".to_string(),
app_name: self.app_name.clone(),
})
.map_err(|_| DotCfgError::NoHomeDir)?;
strategy.config_dir()
}
DirStrategy::Custom(path) => path.clone(),
};
Ok(dir)
}
pub fn file_path(&self) -> Result<PathBuf, DotCfgError> {
let ext = match self.format {
#[cfg(feature = "toml")]
Format::Toml => "toml",
#[cfg(feature = "json")]
Format::Json => "json",
#[cfg(feature = "yaml")]
Format::Yaml => "yaml",
};
Ok(self.dir()?.join(format!("{}.{}", self.filename, ext)))
}
pub fn exists(&self) -> Result<bool, DotCfgError> {
Ok(self.file_path()?.exists())
}
fn ensure_dir(&self) -> Result<(), DotCfgError> {
let dir = self.dir()?;
if !dir.exists() {
fs::create_dir_all(&dir)?;
}
Ok(())
}
pub fn load<T>(&self) -> Result<Option<T>, DotCfgError>
where
T: for<'de> Deserialize<'de>,
{
let path = self.file_path()?;
if !path.exists() {
return Ok(None);
}
let content = fs::read_to_string(&path)?;
let config = match self.format {
#[cfg(feature = "toml")]
Format::Toml => toml::from_str(&content)?,
#[cfg(feature = "json")]
Format::Json => serde_json::from_str(&content)?,
#[cfg(feature = "yaml")]
Format::Yaml => serde_yaml_ng::from_str(&content)?,
};
Ok(Some(config))
}
pub fn load_or_error<T>(&self) -> Result<T, DotCfgError>
where
T: for<'de> Deserialize<'de>,
{
self.load()?.ok_or(DotCfgError::NotFound)
}
pub fn load_or_default<T>(&self) -> Result<T, DotCfgError>
where
T: for<'de> Deserialize<'de> + Serialize + Default,
{
match self.load()? {
Some(cfg) => Ok(cfg),
None => {
let default = T::default();
self.save(&default)?;
Ok(default)
}
}
}
pub fn save<T: Serialize>(&self, config: &T) -> Result<(), DotCfgError> {
self.ensure_dir()?;
let path = self.file_path()?;
let content = match self.format {
#[cfg(feature = "toml")]
Format::Toml => toml::to_string_pretty(config)?,
#[cfg(feature = "json")]
Format::Json => serde_json::to_string_pretty(config)?,
#[cfg(feature = "yaml")]
Format::Yaml => serde_yaml_ng::to_string(config)?,
};
fs::write(&path, content)?;
Ok(())
}
pub fn get(&self, key: &str) -> Result<String, DotCfgError> {
if let Some((_, raw)) = self.env_override(key)? {
return Ok(raw);
}
let path = self.file_path()?;
if !path.exists() {
return Err(DotCfgError::NotFound);
}
let content = fs::read_to_string(&path)?;
match self.format {
#[cfg(feature = "toml")]
Format::Toml => {
let value: toml::Value = toml::from_str(&content)?;
get_toml_value(&value, key)
}
#[cfg(feature = "json")]
Format::Json => {
let value: serde_json::Value = serde_json::from_str(&content)?;
get_json_value(&value, key)
}
#[cfg(feature = "yaml")]
Format::Yaml => {
let value: serde_yaml_ng::Value = serde_yaml_ng::from_str(&content)?;
get_yaml_value(&value, key)
}
}
}
pub fn set(&self, key: &str, value: &str) -> Result<(), DotCfgError> {
let path = self.file_path()?;
match self.format {
#[cfg(feature = "toml")]
Format::Toml => {
let mut table: toml::Value = if path.exists() {
let content = fs::read_to_string(&path)?;
toml::from_str(&content)?
} else {
toml::Value::Table(toml::map::Map::new())
};
set_toml_value(&mut table, key, value)?;
self.ensure_dir()?;
fs::write(&path, toml::to_string_pretty(&table)?)?;
}
#[cfg(feature = "json")]
Format::Json => {
let mut json: serde_json::Value = if path.exists() {
let content = fs::read_to_string(&path)?;
serde_json::from_str(&content)?
} else {
serde_json::Value::Object(serde_json::Map::new())
};
set_json_value(&mut json, key, value)?;
self.ensure_dir()?;
fs::write(&path, serde_json::to_string_pretty(&json)?)?;
}
#[cfg(feature = "yaml")]
Format::Yaml => {
let mut yaml: serde_yaml_ng::Value = if path.exists() {
let content = fs::read_to_string(&path)?;
serde_yaml_ng::from_str(&content)?
} else {
serde_yaml_ng::Value::Mapping(serde_yaml_ng::Mapping::new())
};
set_yaml_value(&mut yaml, key, value)?;
self.ensure_dir()?;
fs::write(&path, serde_yaml_ng::to_string(&yaml)?)?;
}
}
Ok(())
}
pub fn get_as<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<T, DotCfgError> {
if let Some((var, raw)) = self.env_override(key)? {
return env::from_env_str(&var, &raw);
}
let path = self.file_path()?;
if !path.exists() {
return Err(DotCfgError::NotFound);
}
let content = fs::read_to_string(&path)?;
match self.format {
#[cfg(feature = "toml")]
Format::Toml => {
let value: toml::Value = toml::from_str(&content)?;
Ok(get_toml_node(&value, key)?.clone().try_into()?)
}
#[cfg(feature = "json")]
Format::Json => {
let value: serde_json::Value = serde_json::from_str(&content)?;
Ok(serde_json::from_value(get_json_node(&value, key)?.clone())?)
}
#[cfg(feature = "yaml")]
Format::Yaml => {
let value: serde_yaml_ng::Value = serde_yaml_ng::from_str(&content)?;
Ok(serde_yaml_ng::from_value(
get_yaml_node(&value, key)?.clone(),
)?)
}
}
}
pub fn set_val<T: Serialize>(&self, key: &str, value: T) -> Result<(), DotCfgError> {
let path = self.file_path()?;
match self.format {
#[cfg(feature = "toml")]
Format::Toml => {
let new_val = toml::Value::try_from(value)?;
let mut table: toml::Value = if path.exists() {
let content = fs::read_to_string(&path)?;
toml::from_str(&content)?
} else {
toml::Value::Table(toml::map::Map::new())
};
set_toml_node(&mut table, key, new_val)?;
self.ensure_dir()?;
fs::write(&path, toml::to_string_pretty(&table)?)?;
}
#[cfg(feature = "json")]
Format::Json => {
let new_val = serde_json::to_value(value)?;
let mut json: serde_json::Value = if path.exists() {
let content = fs::read_to_string(&path)?;
serde_json::from_str(&content)?
} else {
serde_json::Value::Object(serde_json::Map::new())
};
set_json_node(&mut json, key, new_val)?;
self.ensure_dir()?;
fs::write(&path, serde_json::to_string_pretty(&json)?)?;
}
#[cfg(feature = "yaml")]
Format::Yaml => {
let new_val = serde_yaml_ng::to_value(value)?;
let mut yaml: serde_yaml_ng::Value = if path.exists() {
let content = fs::read_to_string(&path)?;
serde_yaml_ng::from_str(&content)?
} else {
serde_yaml_ng::Value::Mapping(serde_yaml_ng::Mapping::new())
};
set_yaml_node(&mut yaml, key, new_val)?;
self.ensure_dir()?;
fs::write(&path, serde_yaml_ng::to_string(&yaml)?)?;
}
}
Ok(())
}
pub fn delete_file(&self) -> Result<(), DotCfgError> {
let path = self.file_path()?;
if path.exists() {
fs::remove_file(path)?;
}
Ok(())
}
pub fn delete_dir(&self) -> Result<(), DotCfgError> {
let dir = self.dir()?;
if dir.exists() {
fs::remove_dir_all(dir)?;
}
Ok(())
}
}
#[cfg(feature = "toml")]
fn get_toml_node<'a>(value: &'a toml::Value, key: &str) -> Result<&'a toml::Value, DotCfgError> {
let parts: Vec<&str> = key.splitn(2, '.').collect();
match parts.as_slice() {
[field] => value.get(field),
[section, field] => value.get(section).and_then(|s| s.get(field)),
_ => return Err(DotCfgError::InvalidKey(key.to_string())),
}
.ok_or_else(|| DotCfgError::KeyNotFound(key.to_string()))
}
#[cfg(feature = "toml")]
fn get_toml_value(value: &toml::Value, key: &str) -> Result<String, DotCfgError> {
get_toml_node(value, key).map(toml_val_to_string)
}
#[cfg(feature = "toml")]
fn set_toml_node(
value: &mut toml::Value,
key: &str,
new_val: toml::Value,
) -> Result<(), DotCfgError> {
let parts: Vec<&str> = key.splitn(2, '.').collect();
let table = value
.as_table_mut()
.ok_or_else(|| DotCfgError::NotATable("root".to_string()))?;
match parts.as_slice() {
[field] => {
table.insert(field.to_string(), new_val);
}
[section, field] => {
let section_val = table
.entry(section.to_string())
.or_insert_with(|| toml::Value::Table(toml::map::Map::new()));
let section_table = section_val
.as_table_mut()
.ok_or_else(|| DotCfgError::NotATable(section.to_string()))?;
section_table.insert(field.to_string(), new_val);
}
_ => return Err(DotCfgError::InvalidKey(key.to_string())),
}
Ok(())
}
#[cfg(feature = "toml")]
fn set_toml_value(value: &mut toml::Value, key: &str, new_val: &str) -> Result<(), DotCfgError> {
set_toml_node(value, key, toml::Value::String(new_val.to_string()))
}
#[cfg(feature = "toml")]
fn toml_val_to_string(value: &toml::Value) -> String {
match value {
toml::Value::String(s) => s.clone(),
toml::Value::Integer(i) => i.to_string(),
toml::Value::Float(f) => f.to_string(),
toml::Value::Boolean(b) => b.to_string(),
toml::Value::Datetime(d) => d.to_string(),
toml::Value::Array(a) => {
toml::to_string(&toml::Value::Array(a.clone())).unwrap_or_default()
}
toml::Value::Table(t) => toml::to_string(t).unwrap_or_default(),
}
}
#[cfg(feature = "json")]
fn get_json_node<'a>(
value: &'a serde_json::Value,
key: &str,
) -> Result<&'a serde_json::Value, DotCfgError> {
let parts: Vec<&str> = key.splitn(2, '.').collect();
match parts.as_slice() {
[field] => value.get(field),
[section, field] => value.get(section).and_then(|s| s.get(field)),
_ => return Err(DotCfgError::InvalidKey(key.to_string())),
}
.ok_or_else(|| DotCfgError::KeyNotFound(key.to_string()))
}
#[cfg(feature = "json")]
fn get_json_value(value: &serde_json::Value, key: &str) -> Result<String, DotCfgError> {
get_json_node(value, key).map(json_val_to_string)
}
#[cfg(feature = "json")]
fn set_json_node(
value: &mut serde_json::Value,
key: &str,
new_val: serde_json::Value,
) -> Result<(), DotCfgError> {
let parts: Vec<&str> = key.splitn(2, '.').collect();
let obj = value
.as_object_mut()
.ok_or_else(|| DotCfgError::NotATable("root".to_string()))?;
match parts.as_slice() {
[field] => {
obj.insert(field.to_string(), new_val);
}
[section, field] => {
let section_val = obj
.entry(section.to_string())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
let section_obj = section_val
.as_object_mut()
.ok_or_else(|| DotCfgError::NotATable(section.to_string()))?;
section_obj.insert(field.to_string(), new_val);
}
_ => return Err(DotCfgError::InvalidKey(key.to_string())),
}
Ok(())
}
#[cfg(feature = "json")]
fn set_json_value(
value: &mut serde_json::Value,
key: &str,
new_val: &str,
) -> Result<(), DotCfgError> {
set_json_node(value, key, serde_json::Value::String(new_val.to_string()))
}
#[cfg(feature = "json")]
fn json_val_to_string(value: &serde_json::Value) -> String {
match value {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Null => "null".to_string(),
serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
serde_json::to_string(value).unwrap_or_default()
}
}
}
#[cfg(feature = "yaml")]
fn get_yaml_node<'a>(
value: &'a serde_yaml_ng::Value,
key: &str,
) -> Result<&'a serde_yaml_ng::Value, DotCfgError> {
let parts: Vec<&str> = key.splitn(2, '.').collect();
match parts.as_slice() {
[field] => value.get(field),
[section, field] => value.get(section).and_then(|s| s.get(field)),
_ => return Err(DotCfgError::InvalidKey(key.to_string())),
}
.ok_or_else(|| DotCfgError::KeyNotFound(key.to_string()))
}
#[cfg(feature = "yaml")]
fn get_yaml_value(value: &serde_yaml_ng::Value, key: &str) -> Result<String, DotCfgError> {
get_yaml_node(value, key).map(yaml_val_to_string)
}
#[cfg(feature = "yaml")]
fn set_yaml_node(
value: &mut serde_yaml_ng::Value,
key: &str,
new_val: serde_yaml_ng::Value,
) -> Result<(), DotCfgError> {
let parts: Vec<&str> = key.splitn(2, '.').collect();
let map = value
.as_mapping_mut()
.ok_or_else(|| DotCfgError::NotATable("root".to_string()))?;
match parts.as_slice() {
[field] => {
map.insert(serde_yaml_ng::Value::String(field.to_string()), new_val);
}
[section, field] => {
let section_val = map
.entry(serde_yaml_ng::Value::String(section.to_string()))
.or_insert_with(|| serde_yaml_ng::Value::Mapping(serde_yaml_ng::Mapping::new()));
let section_map = section_val
.as_mapping_mut()
.ok_or_else(|| DotCfgError::NotATable(section.to_string()))?;
section_map.insert(serde_yaml_ng::Value::String(field.to_string()), new_val);
}
_ => return Err(DotCfgError::InvalidKey(key.to_string())),
}
Ok(())
}
#[cfg(feature = "yaml")]
fn set_yaml_value(
value: &mut serde_yaml_ng::Value,
key: &str,
new_val: &str,
) -> Result<(), DotCfgError> {
set_yaml_node(
value,
key,
serde_yaml_ng::Value::String(new_val.to_string()),
)
}
#[cfg(feature = "yaml")]
fn yaml_val_to_string(value: &serde_yaml_ng::Value) -> String {
match value {
serde_yaml_ng::Value::String(s) => s.clone(),
serde_yaml_ng::Value::Number(n) => n.to_string(),
serde_yaml_ng::Value::Bool(b) => b.to_string(),
serde_yaml_ng::Value::Null => "null".to_string(),
_ => serde_yaml_ng::to_string(value)
.unwrap_or_default()
.trim_end()
.to_string(),
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
fn set_env(var: &str, val: &str) {
unsafe { std::env::set_var(var, val) }
}
fn env_prefix(suffix: &str) -> String {
format!("dotcfg_t_{}_{}", suffix, std::process::id())
}
#[test]
fn env_var_name_mapping() {
let cfg = DotCfg::new("mytool").with_env_prefix("myapp");
assert_eq!(cfg.env_var_name("port").unwrap(), "MYAPP_PORT");
assert_eq!(cfg.env_var_name("user.name").unwrap(), "MYAPP_USER_NAME");
assert_eq!(cfg.env_var_name("PORT").unwrap(), "MYAPP_PORT");
assert_eq!(
DotCfg::new("mytool")
.with_env_prefix("MyApp")
.env_var_name("port")
.unwrap(),
"MYAPP_PORT"
);
assert!(DotCfg::new("mytool").env_var_name("port").is_none());
}
#[test]
fn env_override_three_way_fallback() {
let prefix = env_prefix("threeway");
let var = format!("{}_PORT", prefix.to_ascii_uppercase());
set_env(&var, "9000");
assert!(
DotCfg::new("mytool")
.env_override("port")
.unwrap()
.is_none()
);
let cfg = DotCfg::new("mytool").with_env_prefix(&prefix);
let (found_var, raw) = cfg.env_override("port").unwrap().unwrap();
assert_eq!(found_var, var);
assert_eq!(raw, "9000");
assert!(cfg.env_override("not_set_anywhere").unwrap().is_none());
}
#[test]
fn env_get_returns_raw_string() {
let prefix = env_prefix("get_raw");
set_env(&format!("{}_USERNAME", prefix.to_ascii_uppercase()), "jane");
let cfg = DotCfg::new("dotcfg_no_such_app").with_env_prefix(&prefix);
assert_eq!(cfg.get("username").unwrap(), "jane");
assert!(matches!(
cfg.get("username_other").unwrap_err(),
DotCfgError::NotFound
));
}
#[test]
fn env_get_as_typed_values() {
let prefix = env_prefix("get_as");
let up = prefix.to_ascii_uppercase();
set_env(&format!("{up}_PORT"), "9000");
set_env(&format!("{up}_DEBUG"), "true");
set_env(&format!("{up}_PLUGINS"), "fmt,lint");
set_env(&format!("{up}_FEATURES_AUTO_UPDATE"), "false");
set_env(&format!("{up}_BAD"), "notanumber");
let cfg = DotCfg::new("dotcfg_no_such_app").with_env_prefix(&prefix);
assert_eq!(cfg.get_as::<u16>("port").unwrap(), 9000);
assert!(cfg.get_as::<bool>("debug").unwrap());
assert_eq!(
cfg.get_as::<Vec<String>>("plugins").unwrap(),
vec!["fmt".to_string(), "lint".to_string()]
);
assert!(!cfg.get_as::<bool>("features.auto_update").unwrap());
assert!(matches!(
cfg.get_as::<u16>("bad").unwrap_err(),
DotCfgError::EnvParse(_, _)
));
}
#[cfg(feature = "toml")]
#[test]
fn toml_val_to_string_variants() {
assert_eq!(toml_val_to_string(&toml::Value::String("hi".into())), "hi");
assert_eq!(toml_val_to_string(&toml::Value::Integer(42)), "42");
assert_eq!(toml_val_to_string(&toml::Value::Boolean(true)), "true");
}
#[cfg(feature = "toml")]
#[test]
fn get_set_toml_helper() {
let mut val = toml::Value::Table(toml::map::Map::new());
set_toml_value(&mut val, "username", "tayo").unwrap();
assert_eq!(get_toml_value(&val, "username").unwrap(), "tayo");
set_toml_value(&mut val, "user.username", "jane").unwrap();
assert_eq!(get_toml_value(&val, "user.username").unwrap(), "jane");
assert!(get_toml_value(&val, "missing").is_err());
}
#[cfg(feature = "toml")]
#[test]
fn toml_node_helpers_roundtrip_typed_values() {
let mut val = toml::Value::Table(toml::map::Map::new());
set_toml_node(&mut val, "port", toml::Value::Integer(8080)).unwrap();
set_toml_node(&mut val, "features.auto_update", toml::Value::Boolean(true)).unwrap();
let port: u16 = get_toml_node(&val, "port")
.unwrap()
.clone()
.try_into()
.unwrap();
assert_eq!(port, 8080);
assert!(
get_toml_node(&val, "features.auto_update")
.unwrap()
.as_bool()
.unwrap()
);
assert_eq!(get_toml_value(&val, "port").unwrap(), "8080");
assert!(get_toml_node(&val, "missing").is_err());
}
#[cfg(feature = "json")]
#[test]
fn json_val_to_string_variants() {
assert_eq!(
json_val_to_string(&serde_json::Value::String("hi".into())),
"hi"
);
assert_eq!(
json_val_to_string(&serde_json::Value::Number(42.into())),
"42"
);
assert_eq!(json_val_to_string(&serde_json::Value::Bool(false)), "false");
}
#[cfg(feature = "json")]
#[test]
fn get_set_json_helper() {
let mut val = serde_json::Value::Object(serde_json::Map::new());
set_json_value(&mut val, "username", "tayo").unwrap();
assert_eq!(get_json_value(&val, "username").unwrap(), "tayo");
set_json_value(&mut val, "user.username", "jane").unwrap();
assert_eq!(get_json_value(&val, "user.username").unwrap(), "jane");
}
#[cfg(feature = "json")]
#[test]
fn json_node_helpers_roundtrip_typed_values() {
let mut val = serde_json::Value::Object(serde_json::Map::new());
set_json_node(&mut val, "weights", serde_json::json!([1, 2, 3])).unwrap();
set_json_node(
&mut val,
"features.auto_update",
serde_json::Value::Bool(true),
)
.unwrap();
let weights: Vec<i32> =
serde_json::from_value(get_json_node(&val, "weights").unwrap().clone()).unwrap();
assert_eq!(weights, vec![1, 2, 3]);
assert!(
get_json_node(&val, "features.auto_update")
.unwrap()
.as_bool()
.unwrap()
);
assert!(get_json_node(&val, "missing").is_err());
}
#[cfg(feature = "yaml")]
#[test]
fn yaml_val_to_string_variants() {
assert_eq!(
yaml_val_to_string(&serde_yaml_ng::Value::String("hi".into())),
"hi"
);
assert_eq!(
yaml_val_to_string(&serde_yaml_ng::Value::Number(42.into())),
"42"
);
assert_eq!(
yaml_val_to_string(&serde_yaml_ng::Value::Bool(false)),
"false"
);
assert_eq!(yaml_val_to_string(&serde_yaml_ng::Value::Null), "null");
}
#[cfg(feature = "yaml")]
#[test]
fn get_set_yaml_helper() {
let mut val = serde_yaml_ng::Value::Mapping(serde_yaml_ng::Mapping::new());
set_yaml_value(&mut val, "username", "tayo").unwrap();
assert_eq!(get_yaml_value(&val, "username").unwrap(), "tayo");
set_yaml_value(&mut val, "user.username", "jane").unwrap();
assert_eq!(get_yaml_value(&val, "user.username").unwrap(), "jane");
assert!(get_yaml_value(&val, "missing").is_err());
}
#[cfg(feature = "yaml")]
#[test]
fn yaml_node_helpers_roundtrip_typed_values() {
let mut val = serde_yaml_ng::Value::Mapping(serde_yaml_ng::Mapping::new());
set_yaml_node(
&mut val,
"plugins",
serde_yaml_ng::Value::Sequence(vec![
serde_yaml_ng::Value::String("fmt".into()),
serde_yaml_ng::Value::String("lint".into()),
]),
)
.unwrap();
set_yaml_node(
&mut val,
"features.auto_update",
serde_yaml_ng::Value::Bool(true),
)
.unwrap();
let plugins: Vec<String> =
serde_yaml_ng::from_value(get_yaml_node(&val, "plugins").unwrap().clone()).unwrap();
assert_eq!(plugins, vec!["fmt", "lint"]);
assert!(
get_yaml_node(&val, "features.auto_update")
.unwrap()
.as_bool()
.unwrap()
);
assert!(get_yaml_node(&val, "missing").is_err());
}
}