preftool-clap 0.2.0

Configuration library for CLI tools/servers.
Documentation
extern crate clap;
extern crate preftool;
#[macro_use]
extern crate bitflags;

use clap::{App, Arg, ArgMatches};
use preftool::*;
use std::collections::HashMap;
use std::ffi::OsString;
use std::rc::Rc;

pub type Validator = dyn Fn(String) -> Result<(), String>;
pub use context::Context;

pub struct ArgBuilder {
  name: ConfigKey,
  long: String,
  help: Option<&'static str>,
  takes_value: bool,
  validate: Option<Rc<Validator>>,
}

impl ArgBuilder {
  pub fn new(name: ConfigKey, long: String) -> Self {
    ArgBuilder {
      name,
      long,
      help: None,
      takes_value: false,
      validate: None,
    }
  }

  pub(crate) fn name(&self) -> &str {
    self.name.as_ref()
  }

  pub fn help(&mut self, help: Option<&'static str>) {
    self.help = help;
  }

  pub fn get_help(&self) -> Option<&str> {
    match self.help {
      None => None,
      Some(s) => Some(s),
    }
  }

  pub fn takes_value(&mut self, takes_value: bool) {
    self.takes_value = takes_value;
  }

  pub fn validate<F>(&mut self, f: F)
  where
    F: Fn(String) -> Result<(), String> + 'static,
  {
    self.validate = Some(Rc::new(f));
  }

  pub(crate) fn partial_clone(&self) -> Self {
    ArgBuilder {
      name: self.name.clone(),
      long: self.long.clone(),
      help: self.help,
      takes_value: self.takes_value,
      validate: None,
    }
  }
}

impl<'a, 'b, 'c> Into<Arg<'a, 'b>> for &'c ArgBuilder
where
  'a: 'b,
  'c: 'a,
{
  fn into(self) -> Arg<'a, 'b> {
    let mut arg = Arg::with_name(self.name.as_ref())
      .long(self.long.as_ref())
      .takes_value(self.takes_value);

    if let Some(help) = self.help {
      arg = arg.help(help);
    }

    if let Some(validate) = &self.validate {
      let validate = validate.clone();
      arg = arg.validator(move |val| validate(val));
    }

    if self.takes_value {
      arg = arg.value_name(self.name.as_ref());
    }

    arg
  }
}

#[derive(Default)]
pub struct AppBuilder {
  args: Vec<ArgBuilder>,
}

// TODO: This should be a trait, so config providers could modify behaivour for "children".
impl AppBuilder {
  pub fn new() -> Self {
    AppBuilder { args: Vec::new() }
  }

  pub fn arg(&mut self, arg: ArgBuilder) {
    self.args.push(arg);
  }

  pub fn adopt(&mut self, builder: AppBuilder) {
    for arg in builder.args.into_iter() {
      self.arg(arg);
    }
  }

  pub fn variants(&mut self, variants: &[AppBuilder]) {
    let mut args = HashMap::new();
    for variant in variants.iter() {
      for arg in variant.args.iter() {
        let name = arg.name();
        let variants = match args.get_mut(name) {
          None => {
            args.insert(name, Vec::new());
            args.get_mut(name).unwrap()
          }

          Some(v) => v,
        };

        variants.push(arg);
      }
    }

    for (_name, args) in args.into_iter() {
      // TODO: Validate that HELP text is the same
      let arg = args.first().unwrap().partial_clone();
      self.arg(arg);
    }
  }
}

pub trait ClapConfig {
  fn configure<'c>(app: &mut AppBuilder, context: Context<'c>, help: Option<&'static str>);
  fn populate<'a, 'c>(
    builder: &mut ConfigurationProviderBuilder,
    matches: &ArgMatches<'a>,
    context: Context<'c>,
  );
}

pub trait AppExt {
  fn get_config<T: ClapConfig>(self) -> DefaultConfigurationProvider;
  fn get_config_from_args<T, I, S>(self, args: I) -> clap::Result<DefaultConfigurationProvider>
  where
    T: ClapConfig,
    I: IntoIterator<Item = S>,
    S: Into<OsString> + Clone;
}

pub trait ClapConfigExt: ClapConfig + Sized {
  fn from_cli<'a, 'b>(app: App<'a, 'b>) -> DefaultConfigurationProvider
  where
    'a: 'b,
  {
    app.get_config::<Self>()
  }

  fn from_cli_args<'a, 'b, I, S>(
    app: App<'a, 'b>,
    args: I,
  ) -> clap::Result<DefaultConfigurationProvider>
  where
    'a: 'b,
    I: IntoIterator<Item = S>,
    S: Into<OsString> + Clone,
  {
    app.get_config_from_args::<Self, I, S>(args)
  }
}

impl<T: ClapConfig + Sized> ClapConfigExt for T {}

impl<'a, 'b> AppExt for App<'a, 'b>
where
  'a: 'b,
{
  fn get_config<T: ClapConfig>(self) -> DefaultConfigurationProvider {
    let mut builder = AppBuilder::new();
    T::configure(&mut builder, Context::new(), None);

    let mut app = self;
    for arg in builder.args.iter() {
      app = app.arg(arg);
    }

    let mut builder = ConfigurationProviderBuilder::new();
    let matches = app.get_matches();
    // println!("Matches: {:#?}", matches);
    T::populate(&mut builder, &matches, Context::new());

    builder.build()
  }

  fn get_config_from_args<T, I, S>(self, args: I) -> clap::Result<DefaultConfigurationProvider>
  where
    T: ClapConfig,
    I: IntoIterator<Item = S>,
    S: Into<OsString> + Clone,
  {
    let mut builder = AppBuilder::new();
    T::configure(&mut builder, Context::new(), None);

    let mut app = self;
    for arg in builder.args.iter() {
      app = app.arg(arg);
    }

    let mut builder = ConfigurationProviderBuilder::new();
    let matches = app.get_matches_from_safe(args)?;
    T::populate(&mut builder, &matches, Context::new());

    Ok(builder.build())
  }
}

pub mod paths {
  use crate::Context;
  use preftool::ConfigKey;

  pub fn arg_name(context: &Context<'_>) -> ConfigKey {
    context.join_path(ConfigKey::separator()).into()
  }

  pub fn arg_long(context: &Context<'_>) -> String {
    context.join_path("-")
  }
}

impl ClapConfig for String {
  fn configure<'c>(app: &mut AppBuilder, context: Context<'c>, help: Option<&'static str>) {
    let name = paths::arg_name(&context);
    let long = paths::arg_long(&context);
    let mut arg = ArgBuilder::new(name, long);
    arg.help(help);
    arg.takes_value(true);

    app.arg(arg);
  }

  fn populate<'a, 'c>(
    builder: &mut ConfigurationProviderBuilder,
    matches: &ArgMatches<'a>,
    context: Context<'c>,
  ) {
    let name = paths::arg_name(&context);
    match matches.value_of(&name) {
      None => (),
      Some(v) => {
        builder.add(name, v);
      }
    }
  }
}

impl<T: ClapConfig> ClapConfig for Option<T> {
  fn configure<'c>(app: &mut AppBuilder, context: Context<'c>, help: Option<&'static str>) {
    T::configure(app, context.optional(), help);
  }

  fn populate<'a, 'c>(
    builder: &mut ConfigurationProviderBuilder,
    matches: &ArgMatches<'a>,
    context: Context<'c>,
  ) {
    T::populate(builder, matches, context.optional());
  }
}

impl ClapConfig for usize {
  fn configure<'c>(app: &mut AppBuilder, context: Context<'c>, help: Option<&'static str>) {
    let name = paths::arg_name(&context);
    let long = paths::arg_long(&context);
    let mut arg = ArgBuilder::new(name, long);
    arg.help(help);
    arg.takes_value(true);
    arg.validate(|s| match s.parse::<usize>() {
      Ok(_) => Ok(()),
      Err(e) => Err(format!("{:?}", e)),
    });

    app.arg(arg);
  }

  fn populate<'a, 'c>(
    builder: &mut ConfigurationProviderBuilder,
    matches: &ArgMatches<'a>,
    context: Context<'c>,
  ) {
    let name = paths::arg_name(&context);
    match matches.value_of(&name) {
      None => (),
      Some(v) => {
        builder.add(name, v);
      }
    }
  }
}

mod context;