commit 8652d9d6cd90114c9e26525c16c3a7a2e2f3e7ee Author: Sean Ervin Date: Mon Aug 8 22:38:05 2022 -0400 Push code diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2ba1afa --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "figment-cliarg-provider" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +figment = "0.10.6" +wild = "2.0.4" +argmap = "1.1.2" \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ce3df27 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Sean Ervin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..6fc35ee --- /dev/null +++ b/readme.md @@ -0,0 +1,22 @@ +# CLI Argument provider for Figment + +This crate is a pretty simple implementation for getting config options from cli arguments. + +--- + +### Usage + +CLI Arguments: +```shell +./program --db.name=my-database --host=my-host --port=5432 --user=my-user --password=my-password +``` + +TOML config: +```toml +[db] +name = "my-database" +host = "localhost" +port = 5432 +user = "my-user" +password = "my-password" +``` \ No newline at end of file diff --git a/src/argument_tree.rs b/src/argument_tree.rs new file mode 100755 index 0000000..88b3a0b --- /dev/null +++ b/src/argument_tree.rs @@ -0,0 +1,93 @@ +use std::{collections::HashMap, slice::Iter, iter::Peekable}; + +use figment::value::{Value, Dict}; + +#[derive(Debug)] +pub enum ArgumentTreeNode { + Leaf(Value), + Branch(HashMap), +} + +impl ArgumentTreeNode { + /// Insert a value into the tree. + pub fn insert(&mut self, keys: &mut Peekable<&mut Iter>, value: Value) { + let key = keys.next().unwrap(); + + match self { + ArgumentTreeNode::Leaf(_) => panic!("Cannot insert into a leaf node"), + ArgumentTreeNode::Branch(children) => { + match children.get_mut(key) { + // If the key is already in the tree, insert into it, + // going farther until the key doesn't exist anymore which + // is where we insert the value. + Some(node) => { + node.insert(keys, value); + } + None => { + // Check if we should insert a leaf or a branch by + // `peek`ing to see if there is a next key. + if keys.peek().is_none() { + let node = ArgumentTreeNode::Leaf(value); + children.insert(key.to_owned(), node); + } else { + let mut node = ArgumentTreeNode::Branch(HashMap::new()); + node.insert(keys, value); + + children.insert(key.to_owned(), node); + } + } + } + } + } + } + + /// Convert the tree into a `Dict`. + fn to_dict(&self) -> Dict { + match self { + ArgumentTreeNode::Leaf(_) => panic!("Cannot convert a leaf node to a dict!"), + ArgumentTreeNode::Branch(children) => { + let mut dict = Dict::new(); + + // Iterate over the children and recursively convert them to `Dict`s. + // When it run into a leaf node, insert its value. + for (key, node) in children.iter() { + match node { + ArgumentTreeNode::Leaf(value) => { + dict.insert(key.to_owned(), value.clone()); + } + _ => { + dict.insert(key.to_owned(), Value::from(node.to_dict())); + } + } + } + dict + } + } + } +} + +#[derive(Debug)] +pub struct ArgumentTree { + pub root: ArgumentTreeNode +} + +impl ArgumentTree { + /// Create a new tree. + pub fn new() -> ArgumentTree { + ArgumentTree { + root: ArgumentTreeNode::Branch(HashMap::new()) + } + } + + /// Insert into the tree. + pub fn insert(&mut self, keys: &mut Iter, value: Value) { + let mut keys = keys.peekable(); + + self.root.insert(&mut keys, value); + } + + /// Convert the tree into a `Dict`. + pub fn to_dict(&mut self) -> Dict { + self.root.to_dict() + } +} \ No newline at end of file diff --git a/src/cli_provider.rs b/src/cli_provider.rs new file mode 100755 index 0000000..38271d2 --- /dev/null +++ b/src/cli_provider.rs @@ -0,0 +1,93 @@ +use figment::{Provider, Metadata, Profile, Error}; +use figment::value::{Map, Dict, Value, Tag}; + +use crate::argument_tree::ArgumentTree; + +/// A provider that fetches its data from a given URL. +pub struct FigmentCliArgsProvider { + /// The profile to emit data to if nesting is disabled. + profile: Option, + args: Vec, +} + +impl FigmentCliArgsProvider { + pub fn new() -> Self { + Self { + profile: None, + args: wild::args().collect(), + } + } +} + +impl Provider for FigmentCliArgsProvider { + /// Returns metadata with kind `Cli Flags`, custom source is the + /// command line arguments separated by spaces. + fn metadata(&self) -> Metadata { + let args = &self.args; + Metadata::named("Cli Flags") + .source(args.join(" ")) + } + + /// Parses the command line arguments into a `Map` and `Value`s. + fn data(&self) -> Result, Error> { + // Parse a `Value` from a `String` + fn parse_from_string(string: &String) -> Value { + // TODO: Other integer types + match string.parse::() { + Ok(i) => Value::Num(Tag::Default, figment::value::Num::I32(i)), + Err(_) => match string.parse::() { + Ok(b) => Value::Bool(Tag::Default, b), + Err(_) => Value::from(string.to_owned()), + }, + } + } + + fn parse_cli(args: &Vec)-> Result { + // TODO: Parse _args as booleans + let (_args, argv) = argmap::parse(args.iter()); + + let mut dict = Dict::new(); + let mut tree = ArgumentTree::new(); + for (key, vals) in argv { + let len = vals.len(); + if len == 0 { + // This is usually where booleans are given (use_cache, replace_torrents, etc.) + dict.insert(key, Value::Bool(Tag::Default, true)); + + continue; + } + + // Parse the string argument values into a `Value` + let val = match len { + 1 => parse_from_string(&vals[0]), + _ => { + let mut vec = Vec::new(); + for val in vals { + vec.push(parse_from_string(&val)); + } + Value::from(vec) + }, + }; + + // Separate the key into its parts and then insert it into the tree + let key_vec = key.split(".").map(|s| s.to_string()).collect::>(); + let mut key_iter = key_vec.iter(); + tree.insert(&mut key_iter, val); + } + + dict.append(&mut tree.to_dict()); + + Ok(dict) + } + + match &self.profile { + // Don't nest: `fetch` into a `Dict`. + Some(profile) => Ok(profile.collect(parse_cli(&self.args)?)), + None => { + let mut map = Map::new(); + map.insert(Profile::default(), parse_cli(&self.args)?); + Ok(map) + } + } + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..64a5357 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +pub mod cli_provider; +pub use cli_provider::FigmentCliArgsProvider; + +mod argument_tree; \ No newline at end of file