Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of config parser #286

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Initial implementation of config parser
pacak committed Aug 29, 2023
commit 88dcb6f9db9c11e018184d41961836ba7270b0e9
27 changes: 25 additions & 2 deletions src/args.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::ffi::OsString;
use std::{ffi::OsString, rc::Rc};

pub(crate) use crate::arg::*;
use crate::{
@@ -37,6 +37,7 @@ pub struct Args<'a> {
name: Option<String>,
#[cfg(feature = "autocomplete")]
c_rev: Option<usize>,
config: Option<Rc<dyn crate::config::Config + 'static>>,
}

impl Args<'_> {
@@ -77,6 +78,12 @@ impl Args<'_> {
self.name = Some(name.to_owned());
self
}

#[must_use]
pub fn with_config(mut self, config: impl crate::config::Config + 'static) -> Self {
self.config = Some(Rc::new(config));
self
}
}

impl<const N: usize> From<&'static [&'static str; N]> for Args<'_> {
@@ -86,6 +93,7 @@ impl<const N: usize> From<&'static [&'static str; N]> for Args<'_> {
#[cfg(feature = "autocomplete")]
c_rev: None,
name: None,
config: None,
}
}
}
@@ -97,6 +105,7 @@ impl<'a> From<&'a [&'a std::ffi::OsStr]> for Args<'a> {
#[cfg(feature = "autocomplete")]
c_rev: None,
name: None,
config: None,
}
}
}
@@ -108,6 +117,7 @@ impl<'a> From<&'a [&'a str]> for Args<'a> {
#[cfg(feature = "autocomplete")]
c_rev: None,
name: None,
config: None,
}
}
}
@@ -119,6 +129,7 @@ impl<'a> From<&'a [String]> for Args<'a> {
#[cfg(feature = "autocomplete")]
c_rev: None,
name: None,
config: None,
}
}
}
@@ -130,6 +141,7 @@ impl<'a> From<&'a [OsString]> for Args<'a> {
#[cfg(feature = "autocomplete")]
c_rev: None,
name: None,
config: None,
}
}
}
@@ -150,6 +162,7 @@ impl Args<'_> {
#[cfg(feature = "autocomplete")]
c_rev: None,
name,
config: None,
}
}
}
@@ -245,7 +258,7 @@ pub use inner::State;
mod inner {
use std::{ops::Range, rc::Rc};

use crate::{error::Message, Args};
use crate::{config::ConfigReader, error::Message, Args};

use super::{split_os_argument, Arg, ArgType, ItemState};
#[derive(Clone, Debug)]
@@ -278,6 +291,8 @@ mod inner {
/// scope starts on the right of the first consumed item and might end before the end
/// of the list, similarly for "commands"
scope: Range<usize>,

pub(crate) config: Option<ConfigReader>,
}

impl State {
@@ -400,6 +415,9 @@ mod inner {
if let Some(name) = args.name {
path.push(name);
}

let config = args.config.map(ConfigReader::new);

State {
item_state,
remaining,
@@ -409,6 +427,7 @@ mod inner {
path,
#[cfg(feature = "autocomplete")]
comp,
config,
}
}
}
@@ -472,6 +491,10 @@ mod inner {
self.remaining
}

pub(crate) fn full_len(&self) -> usize {
self.remaining + self.config.as_ref().map_or(0, |c| c.unconsumed())
}

/// Get an argument from a scope that was not consumed yet
pub(crate) fn get(&self, ix: usize) -> Option<&Arg> {
if self.scope.contains(&ix) && self.item_state.get(ix)?.present() {
162 changes: 162 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
//! The idea of the `Cursors` struct is to be able to keep a state for reading from multiple fields
//! that is relatively cheap to clone

use std::{cell::RefCell, collections::BTreeMap, rc::Rc};

pub trait Config {
fn get(&self, path: &[(&'static str, usize)], name: &'static str, num: usize)
-> Option<String>;
}

impl Config for BTreeMap<String, String> {
fn get(
&self,
path: &[(&'static str, usize)],
name: &'static str,
num: usize,
) -> Option<String> {
if path.is_empty() && num == 0 {
self.get(name).cloned()
} else {
None
}
}
}

#[derive(Clone)]
pub(crate) struct ConfigReader {
config: Rc<dyn Config>,
cursors: Cursors,
unconsumed: usize,
path: Vec<(&'static str, usize)>,
}

impl ConfigReader {
pub(crate) fn new(config: Rc<dyn Config + 'static>) -> Self {
Self {
config,
cursors: Cursors::default(),
unconsumed: 1000000000,
path: Vec::new(),
}
}

pub(crate) fn enter(&mut self, name: &'static str) {
let pos = self.cursors.enter(&self.path, name);
self.path.push((name, pos));
}

pub(crate) fn exit(&mut self) {
self.path.pop();
}
pub(crate) fn get(&mut self, name: &'static str) -> Option<String> {
let pos = self.cursors.get(&self.path, name);

let v = self.config.get(&self.path, name, pos)?;
self.unconsumed -= 1;
Some(v)
}
pub(crate) fn unconsumed(&self) -> usize {
self.unconsumed
}
}

impl std::fmt::Debug for ConfigReader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConfigReader").finish()
}
}

#[derive(Debug, Clone, Default)]
pub(crate) struct Cursors {
tree: Rc<RefCell<Trie>>,
items: Vec<usize>,
}

impl Cursors {
pub(crate) fn enter(&mut self, path: &[(&'static str, usize)], name: &'static str) -> usize {
let mut t = (*self.tree).borrow_mut();
let (pos, children) = t.enter(path, name, self.items.len());
for child in children {
if let Some(v) = self.items.get_mut(child) {
*v = 0;
}
}

loop {
match self.items.get_mut(pos) {
Some(v) => {
let res = *v;
*v += 1;
return res;
}
None => self.items.push(0),
}
}
}

pub(crate) fn get(&mut self, path: &[(&'static str, usize)], name: &'static str) -> usize {
let pos = (*self.tree)
.borrow_mut()
.enter(path, name, self.items.len())
.0;

loop {
match self.items.get_mut(pos) {
Some(v) => {
let res = *v;
*v += 1;
return res;
}
None => self.items.push(0),
}
}
}
}

#[derive(Debug, Default)]
struct Trie {
leaf: Option<usize>,
children: BTreeMap<&'static str, Trie>,
}

impl Trie {
fn enter(
&mut self,
path: &[(&'static str, usize)],
name: &'static str,
fallback: usize,
) -> (usize, impl Iterator<Item = usize> + '_) {
let mut cur = self;

for (p, _) in path {
cur = cur.children.entry(p).or_default();
}
cur = cur.children.entry(name).or_default();
if cur.leaf.is_none() {
cur.leaf = Some(fallback);
}
(
cur.leaf.unwrap(),
cur.children.values().filter_map(|t| t.leaf),
)
}
}

#[test]
fn basic_cursor_logic() {
let mut cursors = Cursors::default();
assert_eq!(0, cursors.get(&[], "hello"));
assert_eq!(0, cursors.get(&[], "bob"));
assert_eq!(1, cursors.get(&[], "hello"));
assert_eq!(2, cursors.get(&[], "hello"));
cursors.enter(&[], "nest");
assert_eq!(0, cursors.get(&[("nest", 0)], "hello"));
assert_eq!(1, cursors.get(&[("nest", 0)], "hello"));
assert_eq!(2, cursors.get(&[("nest", 0)], "hello"));
cursors.enter(&[], "nest");
assert_eq!(0, cursors.get(&[("nest", 1)], "hello"));
assert_eq!(1, cursors.get(&[("nest", 1)], "hello"));
assert_eq!(2, cursors.get(&[("nest", 1)], "hello"));
assert_eq!(3, cursors.get(&[], "hello"));
}
22 changes: 17 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -173,6 +173,7 @@ mod complete_gen;
mod complete_run;
#[cfg(feature = "autocomplete")]
mod complete_shell;
pub mod config;
pub mod doc;
mod error;
mod from_os_str;
@@ -200,8 +201,8 @@ pub mod parsers {
};
#[doc(inline)]
pub use crate::structs::{
ParseCollect, ParseCon, ParseCount, ParseFallback, ParseFallbackWith, ParseLast, ParseMany,
ParseOptional, ParseSome,
ParseCollect, ParseCon, ParseCount, ParseEnter, ParseFallback, ParseFallbackWith,
ParseLast, ParseMany, ParseOptional, ParseSome,
};
}

@@ -222,9 +223,10 @@ use crate::{
params::build_positional,
parsers::{NamedArg, ParseAny, ParseCommand, ParsePositional},
structs::{
ParseCollect, ParseCount, ParseFail, ParseFallback, ParseFallbackWith, ParseGroupHelp,
ParseGuard, ParseHide, ParseLast, ParseMany, ParseMap, ParseOptional, ParseOrElse,
ParsePure, ParsePureWith, ParseSome, ParseUsage, ParseWith, ParseWithGroupHelp,
ParseCollect, ParseCount, ParseEnter, ParseFail, ParseFallback, ParseFallbackWith,
ParseGroupHelp, ParseGuard, ParseHide, ParseLast, ParseMany, ParseMap, ParseOptional,
ParseOrElse, ParsePure, ParsePureWith, ParseSome, ParseUsage, ParseWith,
ParseWithGroupHelp,
},
};

@@ -869,6 +871,13 @@ pub trait Parser<T> {
}
// }}}

fn enter(self, name: &'static str) -> ParseEnter<Self>
where
Self: Sized + Parser<T>,
{
ParseEnter { inner: self, name }
}

// combine
// {{{ fallback
/// Use this value as default if the value isn't present on a command line
@@ -1330,6 +1339,7 @@ pub fn short(short: char) -> NamedArg {
short: vec![short],
env: Vec::new(),
long: Vec::new(),
key: Vec::new(),
help: None,
}
}
@@ -1347,6 +1357,7 @@ pub fn long(long: &'static str) -> NamedArg {
short: Vec::new(),
long: vec![long],
env: Vec::new(),
key: Vec::new(),
help: None,
}
}
@@ -1376,6 +1387,7 @@ pub fn env(variable: &'static str) -> NamedArg {
short: Vec::new(),
long: Vec::new(),
help: None,
key: Vec::new(),
env: vec![variable],
}
}
16 changes: 16 additions & 0 deletions src/params.rs
Original file line number Diff line number Diff line change
@@ -108,6 +108,7 @@ pub struct NamedArg {
pub(crate) short: Vec<char>,
pub(crate) long: Vec<&'static str>,
pub(crate) env: Vec<&'static str>,
pub(crate) key: Vec<&'static str>,
pub(crate) help: Option<Doc>,
}

@@ -161,6 +162,11 @@ impl NamedArg {
self
}

pub fn key(mut self, name: &'static str) -> Self {
self.key.push(name);
self
}

/// Add a help message to a `flag`/`switch`/`argument`
///
/// `bpaf` converts doc comments and string into help by following those rules:
@@ -666,6 +672,14 @@ impl<T> ParseArgument<T> {
return Ok(val);
}

if let Some(config) = &mut args.config {
for name in &self.named.key {
if let Some(val) = config.get(name) {
return Ok(val.into());
}
}
}

if let Some(item) = self.item() {
let missing = MissingItem {
item,
@@ -675,6 +689,8 @@ impl<T> ParseArgument<T> {
Err(Error(Message::Missing(vec![missing])))
} else if let Some(name) = self.named.env.first() {
Err(Error(Message::NoEnv(name)))
} else if let Some(name) = self.named.key.first() {
todo!("complain about name {:?}", name)
} else {
unreachable!()
}
29 changes: 27 additions & 2 deletions src/structs.rs
Original file line number Diff line number Diff line change
@@ -723,8 +723,8 @@ where
match parser.eval(args) {
// we keep including values for as long as we consume values from the argument
// list or at least one value
Ok(val) => Ok(if args.len() < *len {
*len = args.len();
Ok(val) => Ok(if args.full_len() < *len {
*len = args.full_len();
Some(val)
} else {
None
@@ -1142,3 +1142,28 @@ impl<T> Parser<T> for Box<dyn Parser<T>> {
self.as_ref().meta()
}
}

pub struct ParseEnter<T> {
pub(crate) inner: T,
pub(crate) name: &'static str,
}

impl<T, P> Parser<T> for ParseEnter<P>
where
P: Parser<T>,
{
fn eval(&self, args: &mut State) -> Result<T, Error> {
if let Some(config) = &mut args.config {
config.enter(self.name);
}
let r = self.inner.eval(args);
if let Some(config) = &mut args.config {
config.exit();
}
r
}

fn meta(&self) -> Meta {
self.inner.meta()
}
}
117 changes: 117 additions & 0 deletions tests/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use bpaf::config::*;
use bpaf::*;

struct DummyConfig(usize);

impl Config for DummyConfig {
fn get(
&self,
path: &[(&'static str, usize)],
name: &'static str,
num: usize,
) -> Option<String> {
use std::fmt::Write;
let mut res = String::new();

for (name, ix) in path {
if *ix >= self.0 {
return None;
}
write!(&mut res, "{}[{}].", name, ix).ok()?
}
if num >= self.0 {
None
} else {
write!(&mut res, "{}[{}]", name, num).ok()?;
Some(res)
}
}
}

#[test]
fn basic_config() {
let cfg = [("name".into(), "Bob".into()), ("age".into(), "21".into())]
.into_iter()
.collect::<std::collections::BTreeMap<String, String>>();

let name = long("name").key("name").argument::<String>("NAME");

let age = long("age").key("age").argument::<usize>("AGE");
let parser = construct!(name, age).to_options();

let args = Args::from(&[]).with_config(cfg.clone());
let r = parser.run_inner(args).unwrap();
assert_eq!(r.0, "Bob");
assert_eq!(r.1, 21);
}

#[test]
fn many_enter() {
let parser = long("name")
.key("name")
.argument::<String>("NAME")
.many()
.enter("group")
.to_options();

let args = Args::from(&[]).with_config(DummyConfig(4));
let r = parser.run_inner(args).unwrap();

assert_eq!(
r,
[
"group[0].name[0]",
"group[0].name[1]",
"group[0].name[2]",
"group[0].name[3]"
]
);
}

#[test]
fn enter_many() {
let parser = long("name")
.key("name")
.argument::<String>("NAME")
.enter("group")
.many()
.to_options();

let args = Args::from(&[]).with_config(DummyConfig(4));
let r = parser.run_inner(args).unwrap();

assert_eq!(
r,
[
"group[0].name[0]",
"group[1].name[0]",
"group[2].name[0]",
"group[3].name[0]"
]
);
}

#[test]
fn many_enter_many() {
let parser = long("name")
.key("name")
.argument::<String>("NAME")
.many()
.enter("group")
.many()
.map(|x| x.into_iter().flatten().collect::<Vec<_>>())
.to_options();

let args = Args::from(&[]).with_config(DummyConfig(2));
let r = parser.run_inner(args).unwrap();

assert_eq!(
r,
[
"group[0].name[0]",
"group[0].name[1]",
"group[1].name[0]",
"group[1].name[1]",
]
);
}