use crate::{Command, Opt}; use std::{collections::BTreeMap, env}; #[derive(Clone, Debug, Default)] pub struct Map(BTreeMap); impl Map { pub fn new() -> Self { Map(BTreeMap::new()) } pub fn set(&mut self, name: String, v: ContextValue) { self.0.insert(name, v); } pub fn get(&self, name: &str) -> Option<&ContextValue> { self.0.get(name) } } #[derive(Clone, Debug)] pub struct Array(Vec); #[derive(Clone, Debug)] pub enum ContextValue { String(String), Integer(i64), Float(f64), Bool(bool), Map(Map), Array(Array), } #[derive(Clone, Copy, Debug)] pub enum ValueKind { String, Integer, Float, Bool, Map, Array, } impl ContextValue { pub fn string(&self) -> Option { if let Self::String(v) = self { Some(v.clone()) } else { None } } pub fn bool(&self) -> Option { if let Self::Bool(v) = self { Some(*v) } else { None } } pub fn integer(&self) -> Option { if let Self::Integer(v) = self { Some(*v) } else { None } } pub fn float(&self) -> Option { if let Self::Float(v) = self { Some(*v) } else { None } } pub fn map(&self) -> Map { if let Self::Map(v) = self { v.clone() } else { Map::new() } } pub fn array(&self) -> Array { if let Self::Array(v) = self { v.clone() } else { Array(vec![]) } } pub fn kind(&self) -> ValueKind { match self { Self::String(_) => ValueKind::String, Self::Integer(_) => ValueKind::Integer, Self::Float(_) => ValueKind::Float, Self::Bool(_) => ValueKind::Bool, Self::Map(_) => ValueKind::Map, Self::Array(_) => ValueKind::Array, } } pub fn parse_string(value: String, kind: ValueKind) -> Option { match kind { ValueKind::String => Some(Self::String(value)), ValueKind::Integer => value.parse::().ok().map(Self::Integer), ValueKind::Float => value.parse::().ok().map(Self::Float), ValueKind::Bool => value.parse::().ok().map(Self::Bool), ValueKind::Array => None, ValueKind::Map => None, } } } impl From for ContextValue { fn from(fr: String) -> Self { ContextValue::String(fr) } } impl From for ContextValue { fn from(fr: i64) -> Self { ContextValue::Integer(fr) } } impl From for ContextValue { fn from(fr: f64) -> Self { ContextValue::Float(fr) } } impl From for ContextValue { fn from(fr: bool) -> Self { ContextValue::Bool(fr) } } pub struct Context { pub cmd: Command, pub(crate) config_data: Map, pub(crate) env_prefix: Option, pub(crate) env_enabled: bool, } impl Context { pub(crate) fn new(cmd: Command) -> Self { Self { cmd, env_prefix: None, env_enabled: true, config_data: Map::new(), } } pub(crate) fn set_opt>(&mut self, opt: &Opt, value: T) { self.config_data.set(opt.name.clone(), value.into()); } pub(crate) fn set_flag(&mut self, opt: &Opt) { self.config_data.set(opt.name.clone(), true.into()); } /// Checks for the existance of a flag pub fn flag(&self, name: &str) -> bool { self.config_data.get(name).is_some() || self.get_env_value(name).is_some() } fn get_env_value(&self, name: &str) -> Option { if !self.env_enabled { return None; } let name = self .env_prefix .as_ref() .map(|pre| format!("{}_{}", pre, name)) .unwrap_or_else(|| name.to_string()); env::var(name).ok() } /// Returns the value of an option if one exists pub fn get_value(&self, name: &str) -> Option { self.config_data .get(name) .cloned() .or_else(|| self.get_env_value(name).map(ContextValue::String)) } /// Returns a string for the field if it is a string pub fn get_string(&self, name: &str) -> Option { match self.get_value(name) { Some(ContextValue::String(v)) => Some(v), Some(ContextValue::Integer(v)) => Some(v.to_string()), Some(ContextValue::Float(v)) => Some(v.to_string()), Some(ContextValue::Bool(v)) => Some(v.to_string()), _ => None, } } /// Returns a i64 for the field if it is an integer pub fn get_int(&self, name: &str) -> Option { match self.get_value(name) { Some(ContextValue::String(v)) => v.parse().ok(), Some(ContextValue::Integer(v)) => Some(v), Some(ContextValue::Float(v)) => Some(v as i64), Some(ContextValue::Bool(true)) => Some(1), Some(ContextValue::Bool(false)) => Some(0), _ => None, } } /// Returns a f64 for the field if it is a float pub fn get_float(&self, name: &str) -> Option { match self.get_value(name) { Some(ContextValue::String(v)) => v.parse().ok(), Some(ContextValue::Integer(v)) => Some(v as f64), Some(ContextValue::Float(v)) => Some(v), Some(ContextValue::Bool(true)) => Some(1_f64), Some(ContextValue::Bool(false)) => Some(0_f64), _ => None, } } /// Returns a f64 for the field if it is a float pub fn get_bool(&self, name: &str) -> Option { match self.get_value(name) { Some(ContextValue::String(v)) => v.parse().ok(), Some(ContextValue::Integer(0)) => Some(false), Some(ContextValue::Integer(_)) => Some(true), Some(ContextValue::Float(v)) => { if v == 0_f64 { Some(false) } else { Some(true) } } Some(ContextValue::Bool(v)) => Some(v), _ => None, } } /// Can be used to display the automatic help message for the current command. pub fn display_help(&self) { crate::command::print_command_help(&self.cmd, &[]) } pub(crate) fn load_config_file(&mut self, filename: &str) { if std::fs::metadata(filename) .ok() .map(|f| f.is_file()) .unwrap_or(false) { let contents = std::fs::read_to_string(filename).unwrap(); self.load_config_str(&contents); } } } #[cfg(not(feature = "config"))] mod noconfig { use super::Context; impl Context { pub(crate) fn load_config_str(&mut self, _filename: &str) {} } } #[cfg(feature = "config_toml")] mod toml { use super::{Array, Context, ContextValue, Map}; impl From for ContextValue { fn from(fr: toml::Value) -> Self { match fr { toml::Value::String(v) => ContextValue::String(v), toml::Value::Integer(v) => ContextValue::Integer(v), toml::Value::Float(v) => ContextValue::Float(v), toml::Value::Boolean(v) => ContextValue::Bool(v), toml::Value::Array(v) => ContextValue::Array(Array( v.iter().map(|v| ContextValue::from(v.clone())).collect(), )), toml::Value::Table(v) => ContextValue::Map(Map(v .iter() .map(|(k, v)| (k.clone(), ContextValue::from(v.clone()))) .collect())), toml::Value::Datetime(v) => ContextValue::String(v.to_string()), } } } impl Context { pub(crate) fn load_config_str(&mut self, data: &str) { self.config_data = ContextValue::from(toml::from_str::(data).unwrap()) .map() .clone() } } } #[cfg(feature = "config_json")] mod json { use super::{Array, Context, ContextValue, Map}; use serde_json::Value; impl From for ContextValue { fn from(fr: Value) -> Self { match fr { Value::String(v) => ContextValue::String(v), Value::Number(v) if v.is_i64() => ContextValue::Integer(v.as_i64().unwrap_or(0)), Value::Number(v) if v.is_f64() => ContextValue::Float(v.as_f64().unwrap_or(0_f64)), Value::Number(v) => ContextValue::Integer(v.as_i64().unwrap_or(0)), Value::Array(v) => ContextValue::Array(Array( v.iter().map(|v| ContextValue::from(v.clone())).collect(), )), Value::Bool(v) => ContextValue::Bool(v), Value::Null => ContextValue::String("null".to_string()), Value::Object(v) => ContextValue::Map(Map(v .iter() .map(|(k, v)| (k.clone(), ContextValue::from(v.clone()))) .collect())), } } } impl Context { pub(crate) fn load_config_str(&mut self, data: &str) { self.config_data = ContextValue::from(serde_json::from_str::(data).unwrap_or_default()) .map() .clone() } } } #[cfg(test)] mod tests { use crate::{App, Opt}; #[test] fn test_env() { std::env::set_var("thing", "1"); App::new() .opt(Opt::flag("thing").short("-t").long("--t")) .handler(|_, ctx, _| { assert!(ctx.flag("thing")); }) .run_with(vec![]) .unwrap(); std::env::set_var("thing", "one"); App::new() .opt(Opt::scalar("thing").short("-t").long("--t")) .handler(|_, ctx, _| { assert_eq!(ctx.get_string("thing"), Some("one".into())); }) .run_with(vec![]) .unwrap(); std::env::set_var("thing", "one"); App::new() .opt(Opt::scalar("thing").short("t").long("thing")) .handler(|_, ctx, _| { assert_eq!(ctx.get_string("thing"), Some("1".into())); }) .run_with(vec!["-t".into(), "1".into()]) .unwrap(); } #[cfg(feature = "config_toml")] #[test] fn config_load_toml() { use super::{Command, Context}; let cmd = Command::new("xxx"); let mut ctx = Context::new(cmd); ctx.load_config_str( r#" config_thing = "good" num = 15 [nested] something = 14 "#, ); assert_eq!(ctx.get_string("config_thing"), Some("good".into())); } #[cfg(feature = "config_json")] #[test] fn config_load_toml() { use super::{Command, Context}; let cmd = Command::new("xxx"); let mut ctx = Context::new(cmd); ctx.load_config_str( r#" { "config_thing": "good", "num": 15, "nested": { "something": 14 } } "#, ); assert_eq!(ctx.get_string("config_thing"), Some("good".into())); } }