Merge branch 'config-refactor' into 'master'

Config File Handling

See merge request arkham/arkham!4
This commit is contained in:
Joe Bellus 2021-06-21 01:24:33 +00:00
commit ff8b1e9298
10 changed files with 436 additions and 72 deletions

View File

@ -3,4 +3,6 @@ image: "rust:latest"
test:cargo:
script:
- rustc --version && cargo --version # Print version info for debugging
- cargo test --workspace --verbose
- cargo test --workspace
- cargo test --workspace --features config_toml
- cargo test --workspace --features config_json

15
Cargo.lock generated
View File

@ -8,6 +8,9 @@ version = "0.1.0"
dependencies = [
"console",
"criterion",
"serde",
"serde_json",
"toml",
]
[[package]]
@ -469,6 +472,9 @@ name = "serde"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_cbor"
@ -542,6 +548,15 @@ dependencies = [
"serde_json",
]
[[package]]
name = "toml"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
dependencies = [
"serde",
]
[[package]]
name = "ucd-trie"
version = "0.1.3"

View File

@ -9,6 +9,14 @@ edition = "2018"
[dependencies]
# indicatif = "0.15.0"
console = "0.14.0"
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true}
toml = { version="0.5.8", optional = true}
[features]
config = ["serde"]
config_toml = ["toml", "config"]
config_json = ["serde_json", "config"]
[dev-dependencies]
criterion = "0.3"
@ -16,3 +24,4 @@ criterion = "0.3"
[[bench]]
name = "arg_parsing"
harness = false

View File

@ -10,15 +10,12 @@ fn parse_args(c: &mut Criterion) {
"c.json".into(),
"thing".into(),
];
let app = App::new()
let mut app = App::new()
.opt(Opt::scalar("user").short("u").long("user"))
.command(
Command::new("thing")
.opt(Opt::scalar("config").short("c").long("config"))
.handler(|_, ctx, _| {
assert_eq!(ctx.get_value("user"), Some("joe".into()));
assert_eq!(ctx.get_value("config"), Some("c.json".into()));
}),
.handler(|_, _ctx, _| {}),
);
b.iter(|| {

23
examples/config.rs Normal file
View File

@ -0,0 +1,23 @@
use arkham::{App, Command, Context, Opt};
fn main() {
App::new()
.name("Config Test Example")
.config_filename("examples/config.toml")
.command(
Command::new("hello")
.opt(Opt::scalar("name").short("n").long("name"))
.short_desc("Prints a hello message with a passed name")
.handler(hello),
)
.run()
.unwrap();
}
fn hello(_: &App, ctx: &Context, _args: &[String]) {
println!(
"Hello, {}",
ctx.get_string("name")
.unwrap_or_else(|| "unknown".to_string())
);
}

1
examples/config.toml Normal file
View File

@ -0,0 +1 @@
name = "Alice Alisson"

View File

@ -21,7 +21,7 @@ fn main() {
fn fibonacci_handler(_app: &App, ctx: &Context, _args: &[String]) {
let v = fibonacci(
ctx.get_value("count")
ctx.get_string("count")
.unwrap_or("1".to_string())
.parse()
.unwrap_or(1),

View File

@ -1,6 +1,8 @@
use crate::print_command_help;
use super::command::{help, Command, Handler};
use super::context::Context;
use super::opt::{ActiveOpt, Opt, OptError, OptKind};
use super::opt::{Opt, OptError, OptKind};
use std::env;
@ -10,6 +12,7 @@ pub struct App {
name: Option<&'static str>,
version: Option<&'static str>,
pub root: Command,
config_filename: Option<&'static str>,
env_prefix: Option<&'static str>,
}
@ -20,6 +23,7 @@ impl Default for App {
version: None,
root: Command::new("root"),
env_prefix: None,
config_filename: None,
}
}
}
@ -143,16 +147,20 @@ impl App {
/// ```rust
/// use arkham::{App, Command, Context, Opt};
/// App::new()
/// .opt(Opt::flag("name").short("n").long("name"))
/// .opt(Opt::scalar("name").short("n").long("name"))
/// .handler(my_handler)
/// .run_with(vec!["-n".to_string(), "alice".to_string()]);
///
/// fn my_handler(app: &App, ctx: &Context, args: &[String]) {
/// println!("Hello, {}", ctx.get_value("name").unwrap());
/// println!("Hello, {}", ctx.get_string("name").unwrap());
/// }
/// ```
pub fn run_with(&self, args: Vec<String>) -> Result<()> {
run_command(self, &self.root, &args, &mut vec![])
pub fn run_with(&mut self, args: Vec<String>) -> Result<()> {
let mut ctx = Context::new(self.root.clone());
if let Some(filename) = self.config_filename {
ctx.load_config_file(filename);
}
run_command(self, &self.root, &args, &mut ctx)
}
/// Execute the app and any specified handlers based on the arguments passsed to the
@ -168,7 +176,7 @@ impl App {
/// .run();
///
/// fn my_handler(app: &App, ctx: &Context, args: &[String]) {
/// println!("Hello, {}", ctx.get_value("name").unwrap_or_else(|| "unnamed".into()));
/// println!("Hello, {}", ctx.get_string("name").unwrap_or_else(|| "unnamed".into()));
/// }
/// ```
pub fn run(&mut self) -> Result<()> {
@ -178,7 +186,7 @@ impl App {
/// This is the core logic for parsing arguments and executing handlers. It is ran but the App::run
/// and App::run_with functions.
fn run_command(app: &App, cmd: &Command, args: &[String], opts: &mut Vec<ActiveOpt>) -> Result<()> {
fn run_command(app: &App, cmd: &Command, args: &[String], ctx: &mut Context) -> Result<()> {
// Get an iterator for the incomming arguments
let mut args = args.iter();
// We will keep track of any arguments that arent consumed by the current command.
@ -192,11 +200,11 @@ fn run_command(app: &App, cmd: &Command, args: &[String], opts: &mut Vec<ActiveO
if let Some(opt) = cmd.opts.iter().find(|o| &o.long == &arg[2..]) {
match opt.kind {
OptKind::Flag => {
opts.push(ActiveOpt::new(opt.clone(), vec!["".into()]));
ctx.set_flag(opt);
}
OptKind::String => {
if let Some(value) = args.next() {
opts.push(ActiveOpt::new(opt.clone(), vec![value.clone()]));
ctx.set_opt(opt, value.clone());
} else {
return Err(OptError::InvalidOpt(opt.name.clone()));
}
@ -212,11 +220,11 @@ fn run_command(app: &App, cmd: &Command, args: &[String], opts: &mut Vec<ActiveO
if let Some(opt) = cmd.opts.iter().find(|o| &o.short == &arg[1..]) {
match opt.kind {
OptKind::Flag => {
opts.push(ActiveOpt::new(opt.clone(), vec!["".into()]));
ctx.set_flag(opt);
}
OptKind::String => {
if let Some(value) = args.next() {
opts.push(ActiveOpt::new(opt.clone(), vec![value.clone()]));
ctx.set_opt(opt, value.clone());
} else {
return Err(OptError::InvalidOpt(opt.name.clone()));
}
@ -237,7 +245,7 @@ fn run_command(app: &App, cmd: &Command, args: &[String], opts: &mut Vec<ActiveO
.find(|cmd| ignored.iter().any(|a| *a == cmd.name))
{
ignored.retain(|a| *a != cmd.name);
return run_command(app, cmd, &ignored, opts);
return run_command(app, cmd, &ignored, ctx);
}
// Automatic command help display
@ -253,16 +261,34 @@ fn run_command(app: &App, cmd: &Command, args: &[String], opts: &mut Vec<ActiveO
// Execute the command handler
if let Some(handler) = cmd.handler {
let mut ctx = Context::new(cmd.clone(), opts.clone());
ctx.cmd = cmd.clone();
if let Some(prefix) = app.env_prefix {
ctx.env_prefix = Some(prefix.to_string());
}
handler(app, &ctx, &ignored);
} else {
crate::vox::print(app.application_header());
print_command_help(cmd, &vec![])
}
Ok(())
}
#[cfg(not(feature = "config"))]
impl App {
pub fn config_filename(&mut self, _filename: &'static str) -> Self {
panic!("Not compiled with configuration capabilities. Add a config parser as a feature");
}
}
#[cfg(feature = "config")]
impl App {
pub fn config_filename(mut self, filename: &'static str) -> Self {
self.config_filename = Some(filename);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -270,14 +296,13 @@ mod tests {
#[test]
fn test_long_string() {
let args: Vec<String> = vec!["--user".into(), "joe".into()];
let app = App::new()
App::new()
.opt(Opt::scalar("user").short("u").long("user"))
.handler(|_, ctx, _| {
assert_eq!(ctx.get_value("user"), Some("joe".into()));
});
let res = app.run_with(args);
assert!(res.is_ok());
assert_eq!(ctx.get_string("user"), Some("joe".into()));
})
.run_with(args)
.unwrap();
}
#[test]
@ -289,18 +314,18 @@ mod tests {
"c.json".into(),
"thing".into(),
];
let app = App::new()
App::new()
.opt(Opt::scalar("user").short("u").long("user"))
.command(
Command::new("thing")
.opt(Opt::scalar("config").short("c").long("config"))
.handler(|_, ctx, _| {
assert_eq!(ctx.get_value("user"), Some("joe".into()));
assert_eq!(ctx.get_value("config"), Some("c.json".into()));
assert_eq!(ctx.get_string("user"), Some("joe".into()));
assert_eq!(ctx.get_string("config"), Some("c.json".into()));
}),
);
let res = app.run_with(args);
assert!(res.is_ok());
)
.run_with(args)
.unwrap();
}
fn function_handler(_app: &App, _ctx: &Context, _args: &[String]) {

View File

@ -1,32 +1,168 @@
use crate::{
opt::{ActiveOpt, OptKind},
Command,
};
use std::env;
use crate::{Command, Opt};
use std::{collections::BTreeMap, env};
#[derive(Clone, Debug)]
pub struct Map(BTreeMap<String, ContextValue>);
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<ContextValue>);
#[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<String> {
if let Self::String(v) = self {
Some(v.clone())
} else {
None
}
}
pub fn bool(&self) -> Option<bool> {
if let Self::Bool(v) = self {
Some(*v)
} else {
None
}
}
pub fn integer(&self) -> Option<i64> {
if let Self::Integer(v) = self {
Some(*v)
} else {
None
}
}
pub fn float(&self) -> Option<f64> {
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<Self> {
match kind {
ValueKind::String => Some(Self::String(value)),
ValueKind::Integer => value.parse::<i64>().ok().map(|v| Self::Integer(v)),
ValueKind::Float => value.parse::<f64>().ok().map(|v| Self::Float(v)),
ValueKind::Bool => value.parse::<bool>().ok().map(|v| Self::Bool(v)),
ValueKind::Array => None,
ValueKind::Map => None,
}
}
}
impl From<String> for ContextValue {
fn from(fr: String) -> Self {
ContextValue::String(fr)
}
}
impl From<i64> for ContextValue {
fn from(fr: i64) -> Self {
ContextValue::Integer(fr)
}
}
impl From<f64> for ContextValue {
fn from(fr: f64) -> Self {
ContextValue::Float(fr)
}
}
impl From<bool> for ContextValue {
fn from(fr: bool) -> Self {
ContextValue::Bool(fr)
}
}
pub struct Context {
opts: Vec<ActiveOpt>,
cmd: Command,
pub(crate) cmd: Command,
pub(crate) config_data: Map,
pub(crate) env_prefix: Option<String>,
pub(crate) env_enabled: bool,
}
impl Context {
pub(crate) fn new(cmd: Command, opts: Vec<ActiveOpt>) -> Self {
pub(crate) fn new(cmd: Command) -> Self {
Self {
opts,
cmd,
env_prefix: None,
env_enabled: true,
config_data: Map::new(),
}
}
pub(crate) fn set_opt<T: Into<ContextValue>>(&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.opts
.iter()
.any(|o| o.definition.name == name && matches!(o.definition.kind, OptKind::Flag))
|| self.get_env_value(name).is_some()
self.config_data.get(name).is_some() || self.get_env_value(name).is_some()
}
fn get_env_value(&self, name: &str) -> Option<String> {
@ -42,24 +178,157 @@ impl Context {
}
/// Returns the value of an option if one exists
pub fn get_value(&self, name: &str) -> Option<String> {
self.opts
.iter()
.find_map(|o| {
if o.definition.name == name {
Some(o.raw_value.first().cloned())
pub fn get_value(&self, name: &str) -> Option<ContextValue> {
self.config_data
.get(name)
.cloned()
.or(self.get_env_value(name).map(|v| ContextValue::String(v)))
}
/// Returns a string for the field if it is a string
pub fn get_string(&self, name: &str) -> Option<String> {
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<i64> {
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<f64> {
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<bool> {
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 {
None
Some(true)
}
})
.flatten()
.or(self.get_env_value(name))
}
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, &vec![])
}
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<toml::Value> 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::<toml::Value>(data).unwrap())
.map()
.clone()
}
}
}
#[cfg(feature = "config_json")]
mod json {
use super::{Array, Context, ContextValue, Map};
use serde_json::Value;
impl From<Value> 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::<Value>(data).unwrap_or_default())
.map()
.clone()
}
}
}
#[cfg(test)]
@ -80,7 +349,7 @@ mod tests {
App::new()
.opt(Opt::scalar("thing").short("-t").long("--t"))
.handler(|_, ctx, _| {
assert_eq!(ctx.get_value("thing"), Some("one".into()));
assert_eq!(ctx.get_string("thing"), Some("one".into()));
})
.run_with(vec![])
.unwrap();
@ -89,9 +358,47 @@ mod tests {
App::new()
.opt(Opt::scalar("thing").short("t").long("thing"))
.handler(|_, ctx, _| {
assert_eq!(ctx.get_value("thing"), Some("1".into()));
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()));
}
}

View File

@ -106,18 +106,3 @@ pub(crate) enum OptKind {
Flag,
String,
}
#[derive(Clone, Debug)]
pub(crate) struct ActiveOpt {
pub definition: Opt,
pub raw_value: Vec<String>,
}
impl ActiveOpt {
pub fn new(definition: Opt, raw_value: Vec<String>) -> Self {
ActiveOpt {
definition,
raw_value,
}
}
}