From 115fe07e2c11aa72e91a5ce9b028ed1c1ff7d806 Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Thu, 2 Feb 2017 12:03:55 -0800 Subject: Add support for Table/Array and deep merging of configuration values --- src/config.rs | 243 ++++++++++++++++++++++++++++++++++++------------------- src/env.rs | 38 ++++++--- src/file/json.rs | 65 ++++++++------- src/file/mod.rs | 123 +++++++++++++++++++--------- src/file/nil.rs | 6 +- src/file/toml.rs | 60 +++++++------- src/lib.rs | 10 +-- src/source.rs | 4 +- src/value.rs | 65 +++++++++------ 9 files changed, 387 insertions(+), 227 deletions(-) diff --git a/src/config.rs b/src/config.rs index b3e296b..126d31b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,7 +4,7 @@ use source::{Source, SourceBuilder}; use std::error::Error; use std::fmt; use std::borrow::Cow; -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; #[derive(Default, Debug)] pub struct FrozenError { } @@ -22,10 +22,12 @@ impl Error for FrozenError { } // Underlying storage for the configuration -enum ConfigStore<'a> { +enum ConfigStore { Mutable { - defaults: HashMap>, - overrides: HashMap>, + defaults: HashMap, + overrides: HashMap, + + // Ordered list of sources sources: Vec>, }, @@ -34,7 +36,7 @@ enum ConfigStore<'a> { Frozen, } -impl<'a> Default for ConfigStore<'a> { +impl Default for ConfigStore { fn default() -> Self { ConfigStore::Mutable { defaults: HashMap::new(), @@ -44,7 +46,60 @@ impl<'a> Default for ConfigStore<'a> { } } -impl<'a> ConfigStore<'a> { +const KEY_DELIM: char = '.'; + +fn merge_in(r: &mut HashMap, key: &str, value: &Value) { + let key_segments: VecDeque<&str> = key.splitn(2, KEY_DELIM).collect(); + + if key_segments.len() > 1 { + // Ensure there is at least an empty hash map + let key = key_segments[0].to_string(); + if r.contains_key(&key) { + // Coerce to table + match *r.get(&key).unwrap() { + Value::Table(_) => { + // Do nothing; already table + } + + _ => { + // Override with empty table + r.insert(key.clone(), Value::Table(HashMap::new())); + } + } + } else { + // Insert table + r.insert(key.clone(), Value::Table(HashMap::new())); + } + + // Continue to merge + if let Value::Table(ref mut table) = *r.get_mut(&key).unwrap() { + merge_in(table, key_segments[1], value); + } + + return; + } + + // Check if we are setting a table (and if we should do a deep merge) + if let Value::Table(ref table) = *value { + let inner_v = r.get_mut(key); + if let Some(&mut Value::Table(ref mut inner_table)) = inner_v { + merge_in_all(inner_table, table); + + return; + } + } + + // Direct set/override whatever is here + r.insert(key.into(), value.clone()); +} + +fn merge_in_all(r: &mut HashMap, map: &HashMap) { + for (key, value) in map { + merge_in(r, key, value); + } +} + +impl ConfigStore { fn merge(&mut self, source: T) -> Result<(), Box> where T: SourceBuilder { @@ -58,10 +113,10 @@ impl<'a> ConfigStore<'a> { } fn set_default(&mut self, key: &str, value: T) -> Result<(), Box> - where T: Into> + where T: Into { if let ConfigStore::Mutable { ref mut defaults, .. } = *self { - defaults.insert(key.to_lowercase(), value.into()); + merge_in(defaults, &key.to_lowercase(), &value.into()); Ok(()) } else { @@ -70,10 +125,10 @@ impl<'a> ConfigStore<'a> { } fn set(&mut self, key: &str, value: T) -> Result<(), Box> - where T: Into> + where T: Into { if let ConfigStore::Mutable { ref mut overrides, .. } = *self { - overrides.insert(key.to_lowercase(), value.into()); + merge_in(overrides, &key.to_lowercase(), &value.into()); Ok(()) } else { @@ -81,53 +136,38 @@ impl<'a> ConfigStore<'a> { } } - fn get(&self, key: &str) -> Option> { + fn collect(&self) -> Result, Box> { if let ConfigStore::Mutable { ref overrides, ref sources, ref defaults } = *self { - // Check explicit override - if let Some(value) = overrides.get(key) { - return Some(Cow::Borrowed(value)); - } + let mut r = HashMap::::new(); - // Check sources - for source in &mut sources.iter().rev() { - if let Some(value) = source.get(key) { - return Some(value); - } - } + merge_in_all(&mut r, defaults); - // Check explicit defaults - if let Some(value) = defaults.get(key) { - return Some(Cow::Borrowed(value)); + for source in sources { + merge_in_all(&mut r, &source.collect()); } - } - None + merge_in_all(&mut r, overrides); + + Ok(r) + } else { + Err(FrozenError::default().into()) + } } } #[derive(Default)] -pub struct Config<'a> { - store: ConfigStore<'a>, +pub struct Config { + store: ConfigStore, + + /// Top-level table of the cached configuration + /// + /// As configuration sources are merged with `Config::merge`, this + /// cache is updated. + cache: HashMap, } -// TODO(@rust): There must be a way to remove this function and use Value::as_str -#[allow(needless_lifetimes)] -fn value_into_str<'a>(value: Value<'a>) -> Option> { - if let Value::String(value) = value { - Some(value) - } else if let Value::Integer(value) = value { - Some(Cow::Owned(value.to_string())) - } else if let Value::Float(value) = value { - Some(Cow::Owned(value.to_string())) - } else if let Value::Boolean(value) = value { - Some(Cow::Owned(value.to_string())) - } else { - None - } -} - -impl<'a> Config<'a> { - pub fn new() -> Config<'a> { +impl Config { + pub fn new() -> Self { Default::default() } @@ -135,73 +175,73 @@ impl<'a> Config<'a> { pub fn merge(&mut self, source: T) -> Result<(), Box> where T: SourceBuilder { - self.store.merge(source) + self.store.merge(source)?; + self.refresh()?; + + Ok(()) } /// Sets the default value for this key. The default value is only used /// when no other value is provided. pub fn set_default(&mut self, key: &str, value: T) -> Result<(), Box> - where T: Into> + where T: Into { - self.store.set_default(key, value) + self.store.set_default(key, value)?; + self.refresh()?; + + Ok(()) } /// Sets an override for this key. pub fn set(&mut self, key: &str, value: T) -> Result<(), Box> - where T: Into> + where T: Into { - self.store.set(key, value) - } + self.store.set(key, value)?; + self.refresh()?; - pub fn get(&self, key: &str) -> Option> { - self.store.get(key) + Ok(()) } - pub fn get_str(&'a self, key: &str) -> Option> { - // TODO(@rust): This is a bit nasty looking; 3x match and requires this - // odd into_str method - if let Some(value) = self.get(key) { - match value { - Cow::Borrowed(value) => { - match value.as_str() { - Some(value) => { - match value { - Cow::Borrowed(value) => Some(Cow::Borrowed(value)), - Cow::Owned(value) => Some(Cow::Owned(value)), - } - } + /// Refresh the configuration cache with fresh + /// data from associated sources. + /// + /// Configuration is automatically refreshed after a mutation + /// operation (`set`, `merge`, `set_default`, etc.). + pub fn refresh(&mut self) -> Result<(), Box> { + self.cache = self.store.collect()?; - _ => None, - } - } + Ok(()) + } - Cow::Owned(value) => value_into_str(value), - } - } else { - None - } + pub fn get<'a>(&'a self, key: &str) -> Option<&'a Value> { + self.cache.get(key) + } + + pub fn get_str<'a>(&'a self, key: &str) -> Option> { + self.get(key).and_then(Value::as_str) } pub fn get_int(&self, key: &str) -> Option { - // TODO(@rust): Why doesn't .and_then(Value::as_int) work? - self.get(key).and_then(|v| v.as_int()) + self.get(key).and_then(Value::as_int) } pub fn get_float(&self, key: &str) -> Option { - // TODO(@rust): Why doesn't .and_then(Value::as_float) work? - self.get(key).and_then(|v| v.as_float()) + self.get(key).and_then(Value::as_float) } pub fn get_bool(&self, key: &str) -> Option { - // TODO(@rust): Why doesn't .and_then(Value::as_bool) work? - self.get(key).and_then(|v| v.as_bool()) + self.get(key).and_then(Value::as_bool) + } + + pub fn get_map<'a>(&'a self, key: &str) -> Option<&'a HashMap> { + self.get(key).and_then(Value::as_map) } } #[cfg(test)] mod test { - // use std::env; - use super::Config; + use std::collections::HashMap; + use super::{Value, Config}; // Retrieval of a non-existent key #[test] @@ -351,4 +391,41 @@ mod test { assert_eq!(c.get_bool("key_10"), Some(false)); assert_eq!(c.get_bool("key_11"), None); } + + // Deep merge of tables + #[test] + fn test_merge() { + let mut c = Config::new(); + + { + let mut m = HashMap::new(); + m.insert("port".into(), Value::Integer(6379)); + m.insert("address".into(), Value::String("::1".into())); + + c.set("redis", m).unwrap(); + } + + { + let m = c.get_map("redis").unwrap(); + + assert_eq!(m.get("port").unwrap().as_int().unwrap(), 6379); + assert_eq!(m.get("address").unwrap().as_str().unwrap(), "::1"); + } + + { + let mut m = HashMap::new(); + m.insert("address".into(), Value::String("::0".into())); + m.insert("db".into(), Value::Integer(1)); + + c.set("redis", m).unwrap(); + } + + { + let m = c.get_map("redis").unwrap(); + + assert_eq!(m.get("port").unwrap().as_int().unwrap(), 6379); + assert_eq!(m.get("address").unwrap().as_str().unwrap(), "::0"); + assert_eq!(m.get("db").unwrap().as_str().unwrap(), "1"); + } + } } diff --git a/src/env.rs b/src/env.rs index 9743722..60e020e 100644 --- a/src/env.rs +++ b/src/env.rs @@ -1,6 +1,6 @@ use std::env; use std::error::Error; -use std::borrow::Cow; +use std::collections::HashMap; use source; use value::Value; @@ -27,18 +27,32 @@ impl source::SourceBuilder for Environment { } impl source::Source for Environment { - fn get<'a>(&self, key: &str) -> Option> { - let mut env_key = String::new(); - - // Apply prefix - if let Some(ref prefix) = self.prefix { - env_key.push_str(prefix); - env_key.push('_'); + fn collect(&self) -> HashMap { + // Iterate through environment variables + let mut r = HashMap::new(); + + // Make prefix pattern + let prefix_pat = if let Some(ref prefix) = self.prefix { + Some(prefix.clone() + "_".into()) + } else { None }; + + for (key, value) in env::vars() { + let mut key = key.to_string(); + + // Check if key matches prefix + if let Some(ref prefix_pat) = prefix_pat { + if key.starts_with(prefix_pat) { + // Remove the prefix from the key + key = key[prefix_pat.len()..].to_string(); + } else { + // Skip this key + continue; + } + } + + r.insert(key, Value::String(value)); } - env_key.push_str(&key.to_uppercase()); - - // Attempt to retreive environment variable and coerce into a Value - env::var(env_key.clone()).ok().map(Value::from).map(Cow::Owned) + r } } diff --git a/src/file/json.rs b/src/file/json.rs index 5f1c3a2..d193da4 100644 --- a/src/file/json.rs +++ b/src/file/json.rs @@ -2,7 +2,7 @@ use serde_json; use source::Source; use std::error::Error; -use std::borrow::Cow; +use std::collections::HashMap; use value::Value; pub struct Content { @@ -19,50 +19,57 @@ impl Content { } } -fn from_json_value<'a>(value: &serde_json::Value) -> Option> { +fn from_json_value(value: &serde_json::Value) -> Value { match *value { serde_json::Value::String(ref value) => { - Some(Cow::Owned(Value::String(Cow::Borrowed(value)))) + Value::String(value.clone()) } serde_json::Value::Number(ref value) => { if let Some(value) = value.as_i64() { - Some(Cow::Owned(Value::Integer(value))) + Value::Integer(value) } else if let Some(value) = value.as_f64() { - Some(Cow::Owned(Value::Float(value))) + Value::Float(value) } else { - None + unreachable!(); } } - serde_json::Value::Bool(value) => Some(Cow::Owned(Value::Boolean(value))), + serde_json::Value::Bool(value) => Value::Boolean(value), - _ => None, - } -} + serde_json::Value::Object(ref table) => { + let mut m = HashMap::new(); -impl Source for Content { - fn get<'a>(&self, key: &str) -> Option> { - // TODO: Key segment iteration is not something that should be here directly - let key_delim = '.'; - let key_segments = key.split(key_delim); - let mut json_cursor = &self.root; - for segment in key_segments { - match *json_cursor { - serde_json::Value::Object(ref table) => { - if let Some(value) = table.get(segment) { - json_cursor = value; - } - } + for (key, value) in table { + m.insert(key.clone(), from_json_value(value)); + } + + Value::Table(m) + } + + serde_json::Value::Array(ref array) => { + let mut l = Vec::new(); - _ => { - // This is not a table or array - // Traversal is not possible - return None; - } + for value in array { + l.push(from_json_value(value)); } + + Value::Array(l) } - from_json_value(json_cursor) + // TODO: What's left is JSON Null; how should we handle that? + _ => { unimplemented!(); } + } +} + +impl Source for Content { + fn collect(&self) -> HashMap { + if let Value::Table(table) = from_json_value(&self.root) { + table + } else { + // TODO: Better handle a non-object at root + // NOTE: I never want to support that but a panic is bad + panic!("expected object at JSON root"); + } } } diff --git a/src/file/mod.rs b/src/file/mod.rs index 6b9fd5c..e85d082 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -14,6 +14,7 @@ mod toml; #[cfg(feature = "json")] mod json; +#[derive(Clone, Copy)] pub enum FileFormat { /// TOML (parsed with toml) #[cfg(feature = "toml")] @@ -47,53 +48,34 @@ impl FileFormat { } } -pub struct File { +pub trait FileSource { + fn try_build(&self, format: FileFormat) -> Result, Box>; +} + +pub struct FileSourceString(String); + +impl FileSource for FileSourceString { + fn try_build(&self, format: FileFormat) -> Result, Box> { + format.parse(&self.0) + } +} + +pub struct FileSourceFile { /// Basename of configuration file name: String, /// Directory where configuration file is found /// When not specified, the current working directory (CWD) is considered path: Option, - - /// Namespace to restrict configuration from the file - namespace: Option, - - /// Format of file (which dictates what driver to use); Defauts to TOML. - format: FileFormat, - - /// A required File will error if it cannot be found - required: bool, } -impl File { - pub fn new(name: &str, format: FileFormat) -> File { - File { - name: name.into(), - format: format, - required: true, - path: None, - namespace: None, - } - } - - pub fn path(self, path: &str) -> File { - File { path: Some(path.into()), ..self } - } - - pub fn namespace(self, namespace: &str) -> File { - File { namespace: Some(namespace.into()), ..self } - } - - pub fn required(self, required: bool) -> File { - File { required: required, ..self } - } - +impl FileSourceFile { // Find configuration file // Use algorithm similar to .git detection by git - fn find_file(&self) -> Result> { + fn find_file(&self, format: FileFormat) -> Result> { // Build expected configuration file let mut basename = PathBuf::new(); - let extensions = self.format.extensions(); + let extensions = format.extensions(); if let Some(ref path) = self.path { basename.push(path.clone()); @@ -125,11 +107,12 @@ impl File { } } } +} - // Build normally and return error on failure - fn try_build(&self) -> Result, Box> { +impl FileSource for FileSourceFile { + fn try_build(&self, format: FileFormat) -> Result, Box> { // Find file - let filename = self.find_file()?; + let filename = self.find_file(format)?; // Read contents from file let mut file = fs::File::open(filename)?; @@ -137,11 +120,71 @@ impl File { file.read_to_string(&mut text)?; // Parse the file - self.format.parse(&text) + format.parse(&text) + } +} + +pub struct File { + /// Source of the file + source: T, + + /// Namespace to restrict configuration from the file + namespace: Option, + + /// Format of file (which dictates what driver to use); Defauts to TOML. + format: FileFormat, + + /// A required File will error if it cannot be found + required: bool, +} + +impl File { + pub fn from_str(s: &str, format: FileFormat) -> File { + File { + format: format, + required: true, + namespace: None, + source: FileSourceString(s.into()), + } + } +} + +impl File { + pub fn new(name: &str, format: FileFormat) -> File { + File { + format: format, + required: true, + namespace: None, + source: FileSourceFile { + name: name.into(), + path: None, + } + } + } +} + +impl File { + pub fn required(self, required: bool) -> File { + File { required: required, ..self } + } + + // Build normally and return error on failure + fn try_build(&self) -> Result, Box> { + self.source.try_build(self.format) + } +} + +impl File { + pub fn path(self, path: &str) -> Self { + File { source: FileSourceFile { path: Some(path.into()), ..self.source } , ..self } + } + + pub fn namespace(self, namespace: &str) -> Self { + File { namespace: Some(namespace.into()), ..self } } } -impl SourceBuilder for File { +impl SourceBuilder for File { // Use try_build but only pass an error through if this source // is required fn build(&self) -> Result, Box> { diff --git a/src/file/nil.rs b/src/file/nil.rs index f494af4..f6d801a 100644 --- a/src/file/nil.rs +++ b/src/file/nil.rs @@ -1,4 +1,4 @@ -use std::borrow::Cow; +use std::collections::HashMap; use source::Source; use value::Value; @@ -7,7 +7,7 @@ use value::Value; pub struct Nil {} impl Source for Nil { - fn get<'a>(&self, _: &str) -> Option> { - None + fn collect(&self) -> HashMap { + HashMap::new() } } diff --git a/src/file/toml.rs b/src/file/toml.rs index b2a8fe8..4de23ef 100644 --- a/src/file/toml.rs +++ b/src/file/toml.rs @@ -1,6 +1,6 @@ use toml; use source::Source; -use std::borrow::Cow; +use std::collections::HashMap; use std::error::Error; use value::Value; @@ -20,39 +20,43 @@ impl Content { } } -fn from_toml_value<'a>(value: &toml::Value) -> Option> { +fn from_toml_value(value: &toml::Value) -> Value { match *value { - toml::Value::String(ref value) => Some(Cow::Owned(Value::String(Cow::Borrowed(value)))), - toml::Value::Float(value) => Some(Cow::Owned(Value::Float(value))), - toml::Value::Integer(value) => Some(Cow::Owned(Value::Integer(value))), - toml::Value::Boolean(value) => Some(Cow::Owned(Value::Boolean(value))), + toml::Value::String(ref value) => Value::String(value.clone()), + toml::Value::Float(value) => Value::Float(value), + toml::Value::Integer(value) => Value::Integer(value), + toml::Value::Boolean(value) => Value::Boolean(value), - _ => None, + toml::Value::Table(ref table) => { + let mut m = HashMap::new(); + + for (key, value) in table { + m.insert(key.clone(), from_toml_value(value)); + } + + Value::Table(m) + } + + toml::Value::Array(ref array) => { + let mut l = Vec::new(); + + for value in array { + l.push(from_toml_value(value)); + } + + Value::Array(l) + } + + _ => { unimplemented!(); } } } impl Source for Content { - fn get<'a>(&self, key: &str) -> Option> { - // TODO: Key segment iteration is not something that should be here directly - let key_delim = '.'; - let key_segments = key.split(key_delim); - let mut toml_cursor = &self.root; - for segment in key_segments { - match *toml_cursor { - toml::Value::Table(ref table) => { - if let Some(value) = table.get(segment) { - toml_cursor = value; - } - } - - _ => { - // This is not a table or array - // Traversal is not possible - return None; - } - } + fn collect(&self) -> HashMap { + if let Value::Table(table) = from_toml_value(&self.root) { + table + } else { + unreachable!(); } - - from_toml_value(toml_cursor) } } diff --git a/src/lib.rs b/src/lib.rs index 8af61c3..5f8957a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,11 +65,11 @@ pub use value::Value; pub use config::Config; // Global configuration -static mut CONFIG: Option>> = None; +static mut CONFIG: Option> = None; static CONFIG_INIT: Once = ONCE_INIT; // Get the global configuration instance -pub fn global() -> &'static mut RwLock> { +pub fn global() -> &'static mut RwLock { unsafe { CONFIG_INIT.call_once(|| { CONFIG = Some(Default::default()); @@ -86,18 +86,18 @@ pub fn merge(source: T) -> Result<(), Box> } pub fn set_default(key: &str, value: T) -> Result<(), Box> - where T: Into> + where T: Into { global().write().unwrap().set_default(key, value) } pub fn set(key: &str, value: T) -> Result<(), Box> - where T: Into> + where T: Into { global().write().unwrap().set(key, value) } -pub fn get<'a>(key: &str) -> Option> { +pub fn get<'a>(key: &str) -> Option<&'a Value> { // TODO(~): Should this panic! or return None with an error message? // Make an issue if you think it should be an error message. let r = global().read().unwrap(); diff --git a/src/source.rs b/src/source.rs index bc1dd40..e136176 100644 --- a/src/source.rs +++ b/src/source.rs @@ -1,10 +1,10 @@ use std::error::Error; -use std::borrow::Cow; +use std::collections::HashMap; use value::Value; pub trait Source { - fn get<'a>(&self, key: &str) -> Option>; + fn collect(&self) -> HashMap; } pub trait SourceBuilder { diff --git a/src/value.rs b/src/value.rs index b66c721..5228d91 100644 --- a/src/value.rs +++ b/src/value.rs @@ -6,26 +6,27 @@ use std::borrow::Cow; /// /// Has an underlying or native type that comes from the configuration source /// but will be coerced into the requested type. -#[derive(Debug, Clone)] -pub enum Value<'a> { - String(Cow<'a, str>), +#[derive(Debug, Clone, PartialEq)] +pub enum Value { + String(String), Integer(i64), Float(f64), Boolean(bool), - Table(HashMap>), - Array(Vec>), + Table(HashMap), + Array(Vec), } -impl<'a> Value<'a> { +impl Value { /// Gets the underlying value as a string, performing a conversion only if neccessary. - pub fn as_str(&'a self) -> Option> { + #[allow(needless_lifetimes)] + pub fn as_str<'a>(&'a self) -> Option> { match *self { - Value::String(ref value) => Some(Cow::Borrowed(&*value)), - Value::Integer(value) => Some(value.to_string().into()), - Value::Float(value) => Some(value.to_string().into()), - Value::Boolean(value) => Some(value.to_string().into()), + Value::String(ref value) => Some(Cow::Borrowed(value)), + Value::Integer(value) => Some(Cow::Owned(value.to_string())), + Value::Float(value) => Some(Cow::Owned(value.to_string())), + Value::Boolean(value) => Some(Cow::Owned(value.to_string())), - _ => unimplemented!(), + _ => None, } } @@ -44,7 +45,7 @@ impl<'a> Value<'a> { } } - _ => unimplemented!(), + _ => None, } } @@ -56,7 +57,7 @@ impl<'a> Value<'a> { Value::Boolean(value) => Some(if value { 1 } else { 0 }), Value::Float(value) => Some(value.round() as i64), - _ => unimplemented!(), + _ => None, } } @@ -68,7 +69,15 @@ impl<'a> Value<'a> { Value::Integer(value) => Some(value as f64), Value::Boolean(value) => Some(if value { 1.0 } else { 0.0 }), - _ => unimplemented!(), + _ => None, + } + } + + /// Gets the underlying type as a map; only works if the type is actually a map. + pub fn as_map(&self) -> Option<&HashMap> { + match *self { + Value::Table(ref value) => Some(value), + _ => None } } } @@ -76,32 +85,38 @@ impl<'a> Value<'a> { // Generalized construction from type into variant is needed // for setting configuration values -impl<'a> From for Value<'a> { - fn from(value: String) -> Value<'a> { +impl From for Value { + fn from(value: String) -> Value { Value::String(value.into()) } } -impl<'a> From<&'a str> for Value<'a> { - fn from(value: &'a str) -> Value<'a> { +impl<'a> From<&'a str> for Value { + fn from(value: &'a str) -> Value { Value::String(value.into()) } } -impl<'a> From for Value<'a> { - fn from(value: i64) -> Value<'a> { +impl From for Value { + fn from(value: i64) -> Value { Value::Integer(value) } } -impl<'a> From for Value<'a> { - fn from(value: f64) -> Value<'a> { +impl From for Value { + fn from(value: f64) -> Value { Value::Float(value) } } -impl<'a> From for Value<'a> { - fn from(value: bool) -> Value<'a> { +impl From for Value { + fn from(value: bool) -> Value { Value::Boolean(value) } } + +impl From> for Value { + fn from(value: HashMap) -> Value { + Value::Table(value) + } +} -- cgit v1.2.3