From 0a6916db9cd63e4d87b03ec0ded31f246c863dc9 Mon Sep 17 00:00:00 2001 From: David McNeil Date: Tue, 21 Apr 2020 10:07:22 -0500 Subject: [PATCH] Public api cleanup Signed-off-by: David McNeil --- README.md | 7 - configopt-derive/README.md | 2 - configopt-derive/src/configopt_type.rs | 108 +++++++------ .../generate/configopt_defaults.rs | 5 +- .../src/configopt_type/generate/core.rs | 72 +++++---- .../generate/handle_config_files.rs | 1 - configopt/README.md | 2 - configopt/src/lib.rs | 142 ++++++++++-------- configopt/tests/test_all_types.rs | 40 +++-- configopt/tests/test_bool.rs | 96 ++++++++++++ 10 files changed, 308 insertions(+), 167 deletions(-) create mode 100644 configopt/tests/test_bool.rs diff --git a/README.md b/README.md index 8a13eec..d947033 100644 --- a/README.md +++ b/README.md @@ -1,8 +1 @@ # ConfigOpt - -## TODO -- Modify configopt_type_derive's `TryFrom` if type does not implement `Debug` -- How to set booleans -- How to handle excludes in clap -- How to handle subcommands -- How to handle flattened structures \ No newline at end of file diff --git a/configopt-derive/README.md b/configopt-derive/README.md index 00a01f3..ad09c3d 100644 --- a/configopt-derive/README.md +++ b/configopt-derive/README.md @@ -1,3 +1 @@ # ConfigOpt Derive - -TODO \ No newline at end of file diff --git a/configopt-derive/src/configopt_type.rs b/configopt-derive/src/configopt_type.rs index f336fa2..9b3199b 100644 --- a/configopt-derive/src/configopt_type.rs +++ b/configopt-derive/src/configopt_type.rs @@ -129,7 +129,7 @@ impl ConfigOptConstruct { Self::Struct(_, parsed_fields) => { let configopt_take = generate::core::take(&parsed_fields, &other); let configopt_patch = generate::core::patch(&parsed_fields, &other); - // let configopt_merge = merge(&parsed_fields); + let configopt_take_for = generate::core::take_for(&parsed_fields, &other); // let configopt_clear = clear(&parsed_fields); // let configopt_is_empty = is_empty(&parsed_fields); // let configopt_is_complete = is_complete(&parsed_fields); @@ -158,10 +158,10 @@ impl ConfigOptConstruct { #configopt_patch } - // /// Take each field from `self` and set it in `other` - // pub fn merge(&mut self, other: &mut #ident) { - // #configopt_merge - // } + /// Take each field from `self` and set it in `other` + pub fn take_for(&mut self, other: &mut #ident) { + #configopt_take_for + } // /// Clear all fields from `self` // pub fn clear(&mut self) { @@ -177,15 +177,6 @@ impl ConfigOptConstruct { // pub fn is_complete(&self) -> bool { // #configopt_is_complete // } - - pub fn maybe_generate_config_file_and_exit(&mut self) { - #handle_config_files_generate - } - - - pub fn patch_with_config_files(&mut self) -> std::result::Result<&mut #configopt_ident, ::std::io::Error> { - #handle_config_files_patch - } } // #lints @@ -204,18 +195,26 @@ impl ConfigOptConstruct { // } #lints - impl ::configopt::ConfigOpt for #configopt_ident {} + impl ::std::convert::TryFrom<&::std::path::Path> for #configopt_ident { + type Error = ::std::io::Error; - #lints - impl ::configopt::ConfigOpt for #ident {} + fn try_from(path: &::std::path::Path) -> ::std::result::Result { + let contents = ::std::fs::read_to_string(path)?; + Ok(::toml::from_str(&contents)?) + } + } #lints - impl ::configopt::TomlConfigGenerator for #configopt_ident { - fn toml_config_with_prefix(&self, serde_prefix: &[String]) -> String { - use ::structopt::StructOpt; + impl> ::std::convert::TryFrom<&[T]> for #configopt_ident { + type Error = ::std::io::Error; - let app = #ident::clap(); - #toml_config_generator_with_prefix + fn try_from(paths: &[T]) -> ::std::result::Result { + let mut result = #configopt_ident::default(); + for path in paths { + let mut other = #configopt_ident::try_from(path.as_ref())?; + result.take(&mut other); + } + Ok(result) } } @@ -232,26 +231,27 @@ impl ConfigOptConstruct { } #lints - impl ::std::convert::TryFrom<&::std::path::Path> for #configopt_ident { - type Error = ::std::io::Error; + impl ::configopt::ConfigOptType for #configopt_ident { + fn maybe_generate_config_file_and_exit(&mut self) { + #handle_config_files_generate + } - fn try_from(path: &::std::path::Path) -> ::std::result::Result { - let contents = ::std::fs::read_to_string(path)?; - Ok(::toml::from_str(&contents)?) + fn patch_with_config_files(&mut self) -> std::result::Result<&mut #configopt_ident, ::std::io::Error> { + #handle_config_files_patch + } + + fn toml_config_with_prefix(&self, serde_prefix: &[String]) -> String { + let app = #ident::clap(); + #toml_config_generator_with_prefix } } #lints - impl> ::std::convert::TryFrom<&[T]> for #configopt_ident { - type Error = ::std::io::Error; + impl ::configopt::ConfigOpt for #ident { + type ConfigOptType = #configopt_ident; - fn try_from(paths: &[T]) -> ::std::result::Result { - let mut result = #configopt_ident::default(); - for path in paths { - let mut other = #configopt_ident::try_from(path.as_ref())?; - result.take(&mut other); - } - Ok(result) + fn take(&mut self, configopt: &mut Self::ConfigOptType) { + configopt.take_for(self) } } } @@ -265,8 +265,18 @@ impl ConfigOptConstruct { generate::configopt_defaults::for_enum(&parsed_variants); quote! { #lints - impl #configopt_ident { - pub fn maybe_generate_config_file_and_exit(&mut self) { + impl ::configopt::ConfigOptDefaults for #configopt_ident { + fn arg_default(&self, arg_path: &[String]) -> Option<::std::ffi::OsString> { + match self { + #configopt_defaults_variant + _ => None, + } + } + } + + #lints + impl ::configopt::ConfigOptType for #configopt_ident { + fn maybe_generate_config_file_and_exit(&mut self) { match self { #handle_config_files_generate _ => {} @@ -274,30 +284,28 @@ impl ConfigOptConstruct { } - pub fn patch_with_config_files(&mut self) -> std::result::Result<&mut #configopt_ident, ::std::io::Error> { + fn patch_with_config_files(&mut self) -> std::result::Result<&mut #configopt_ident, ::std::io::Error> { match self { #handle_config_files_patch _ => {} } Ok(self) } - } - #lints - impl ::configopt::ConfigOpt for #configopt_ident {} + fn toml_config_with_prefix(&self, serde_prefix: &[String]) -> String { + todo!() + } + } #lints - impl ::configopt::ConfigOpt for #ident {} + impl ::configopt::ConfigOpt for #ident { + type ConfigOptType = #configopt_ident; - #lints - impl ::configopt::ConfigOptDefaults for #configopt_ident { - fn arg_default(&self, arg_path: &[String]) -> Option<::std::ffi::OsString> { - match self { - #configopt_defaults_variant - _ => None, - } + fn take(&mut self, configopt: &mut Self::ConfigOptType) { + todo!() } } + } } } diff --git a/configopt-derive/src/configopt_type/generate/configopt_defaults.rs b/configopt-derive/src/configopt_type/generate/configopt_defaults.rs index f29dead..7007c7e 100644 --- a/configopt-derive/src/configopt_type/generate/configopt_defaults.rs +++ b/configopt-derive/src/configopt_type/generate/configopt_defaults.rs @@ -84,7 +84,7 @@ pub fn for_struct(fields: &[ParsedField]) -> TokenStream { let normal_fields = fields.iter().filter(|f| !f.flatten() && !f.subcommand()); let normal_fields = normal_fields .map(|field| { - let arg_name = field.serde_name(); + let arg_name = field.structopt_name(); let to_default = to_default(field); quote! { #arg_name => #to_default, @@ -137,7 +137,6 @@ pub fn for_enum(variants: &[ParsedVariant]) -> TokenStream { let full_configopt_ident = variant.full_configopt_ident(); let span = variant.span(); let structopt_name = variant.structopt_name(); - // TODO: Handle other variants match variant.field_type() { FieldType::Unit => { quote_spanned! {span=> @@ -153,7 +152,7 @@ pub fn for_enum(variants: &[ParsedVariant]) -> TokenStream { } FieldType::Named => { quote_spanned! {span=> - // TODO + // TODO: Actually lookup the values #full_configopt_ident{..} => None, } } diff --git a/configopt-derive/src/configopt_type/generate/core.rs b/configopt-derive/src/configopt_type/generate/core.rs index 9fed6db..89d0c8e 100644 --- a/configopt-derive/src/configopt_type/generate/core.rs +++ b/configopt-derive/src/configopt_type/generate/core.rs @@ -18,12 +18,12 @@ pub fn take(fields: &[ParsedField], other: &Ident) -> TokenStream { } else { match field.structopt_ty() { StructOptTy::Vec => quote_spanned! {span=> - // TODO: Should it be wrapped in `Option`? if !#other_field.is_empty() { ::std::mem::swap(&mut #self_field, &mut #other_field); } }, - StructOptTy::Bool | StructOptTy::Option + StructOptTy::Bool + | StructOptTy::Option | StructOptTy::OptionOption | StructOptTy::OptionVec | StructOptTy::Other => { @@ -54,12 +54,12 @@ pub fn patch(fields: &[ParsedField], other: &Ident) -> TokenStream { } else { match field.structopt_ty() { StructOptTy::Vec => quote_spanned! {span=> - // TODO: Should it be wrapped in `Option`? if #self_field.is_empty() { ::std::mem::swap(&mut #self_field, &mut #other_field); } }, - StructOptTy::Bool | StructOptTy::Option + StructOptTy::Bool + | StructOptTy::Option | StructOptTy::OptionOption | StructOptTy::OptionVec | StructOptTy::Other => { @@ -75,28 +75,48 @@ pub fn patch(fields: &[ParsedField], other: &Ident) -> TokenStream { .collect() } -// fn merge(fields: &[ParsedField]) -> TokenStream { -// fields -// .iter() -// .map(|field| { -// let ident = field.ident(); -// let span = field.span(); -// if field.flatten() { -// quote_spanned! {span=> -// if let Some(mut val) = self.#ident.take() { -// val.merge(&mut other.#ident) -// } -// } -// } else { -// quote_spanned! {span=> -// if let Some(val) = self.#ident.take() { -// other.#ident = val; -// } -// } -// } -// }) -// .collect() -// } +pub fn take_for(fields: &[ParsedField], other: &Ident) -> TokenStream { + fields + .iter() + .map(|field| { + let field_ident = field.ident(); + let span = field.span(); + let self_field = quote! {self.#field_ident}; + let other_field = quote! {#other.#field_ident}; + if field.flatten() { + quote_spanned! {span=> + #self_field.take_for(&mut #other_field); + } + } else if field.subcommand() { + quote_spanned! {span=> + // TODO + } + } else { + match field.structopt_ty() { + StructOptTy::Vec => quote_spanned! {span=> + if !#self_field.is_empty() { + ::std::mem::swap(&mut #other_field, &mut #self_field); + } + }, + StructOptTy::Option | StructOptTy::OptionOption | StructOptTy::OptionVec => { + quote_spanned! {span=> + if #self_field.is_some() { + #other_field = #self_field.take(); + } + } + } + StructOptTy::Bool | StructOptTy::Other => { + quote_spanned! {span=> + if let Some(value) = #self_field.take() { + #other_field = value; + } + } + } + } + } + }) + .collect() +} // fn clear(fields: &[ParsedField]) -> TokenStream { // fields diff --git a/configopt-derive/src/configopt_type/generate/handle_config_files.rs b/configopt-derive/src/configopt_type/generate/handle_config_files.rs index 3e957f3..969a78e 100644 --- a/configopt-derive/src/configopt_type/generate/handle_config_files.rs +++ b/configopt-derive/src/configopt_type/generate/handle_config_files.rs @@ -13,7 +13,6 @@ pub fn generate_for_struct(parsed: &[ParsedField]) -> TokenStream { quote! { if self.generate_config.unwrap_or_default() { use ::std::io::Write; - use ::configopt::TomlConfigGenerator; let out = ::std::io::stdout(); writeln!(&mut out.lock(), "{}", self.toml_config()).expect("Error writing Error to stdout"); ::std::process::exit(0); diff --git a/configopt/README.md b/configopt/README.md index 1ee8f36..d947033 100644 --- a/configopt/README.md +++ b/configopt/README.md @@ -1,3 +1 @@ # ConfigOpt - -TODO \ No newline at end of file diff --git a/configopt/src/lib.rs b/configopt/src/lib.rs index 5df4ced..c45db8a 100644 --- a/configopt/src/lib.rs +++ b/configopt/src/lib.rs @@ -4,7 +4,7 @@ mod configopt_defaults_trait; use arena_trait::Arena; use colosseum::{sync::Arena as SyncArena, unsync::Arena as UnsyncArena}; use lazy_static::lazy_static; -use std::{env, ffi::OsString}; +use std::ffi::OsString; use structopt::{ clap::{App, Result as ClapResult}, StructOpt, @@ -18,15 +18,14 @@ lazy_static! { } // This is very hacky. It reaches deep into clap internals to set the default values, but it works! +// We need to set the defaults to prevent the CLI parsing from failing when a required argument is +// not on the CLI but it is set in a config file. fn set_defaults_impl<'a>( app: &mut App<'_, 'a>, arg_path: &mut Vec, defaults: &impl ConfigOptDefaults, arena: &'a impl Arena, ) { - for _ in &app.p.flags { - // TODO: How to set the default of a flag - } for arg in &mut app.p.opts { arg_path.push(String::from(arg.b.name)); if let Some(default) = defaults.arg_default(arg_path.as_slice()) { @@ -49,35 +48,65 @@ fn set_defaults_impl<'a>( } } -pub fn env_args_contains(s: &[&str]) -> bool { - for argument in env::args() { - if s.contains(&argument.as_str()) { - return true; - } - } - false +/// Set the defaults for a `clap::App` +pub fn set_defaults(app: &mut App<'_, 'static>, defaults: &impl ConfigOptDefaults) { + let mut arg_path = Vec::new(); + let arena = &*DEFAULT_VALUE_STORE; + set_defaults_impl(app, &mut arg_path, defaults, arena); } -/// TODO -pub trait TomlConfigGenerator { +pub trait ConfigOptType: ConfigOptDefaults + StructOpt { + /// If the `--generate-config` flag is set output the current configuration to stdout and exit. + fn maybe_generate_config_file_and_exit(&mut self); + + /// Patch with values from the `--config-files` argument + fn patch_with_config_files(&mut self) -> std::result::Result<&mut Self, ::std::io::Error>; + + fn toml_config_with_prefix(&self, serde_prefix: &[String]) -> String; + + /// Generate TOML configuration. fn toml_config(&self) -> String { self.toml_config_with_prefix(&[]) } - - fn toml_config_with_prefix(&self, serde_prefix: &[String]) -> String; } -/// TODO -/// --generate-config -/// --config-file pub trait ConfigOpt: Sized + StructOpt { - /// Construct an instance of a `structopt` struct using a set of defaults + type ConfigOptType: ConfigOptType; + + /// Set default values. Then gets the struct from the command line arguments. Print the error + /// message and quit the program in case of failure. fn from_args_with_defaults(defaults: &impl ConfigOptDefaults) -> Self { - from_args_with_defaults(defaults) + let mut app = Self::clap(); + // An arena allocator is used to extend the lifetimes of the default value strings. + let arena = UnsyncArena::new(); + let mut arg_path = Vec::new(); + set_defaults_impl(&mut app, &mut arg_path, defaults, &arena); + let matches = app.get_matches(); + Self::from_clap(&matches) } - /// TODO - fn from_iter_safe_with_defaults( + /// Set default values. Then gets the struct from any iterator such as a Vec of your making. + /// Print the error message and quit the program in case of failure. + fn from_iter_with_defaults(iter: I, defaults: &impl ConfigOptDefaults) -> Self + where + I: IntoIterator, + I::Item: Into + Clone, + { + let mut app = Self::clap(); + // An arena allocator is used to extend the lifetimes of the default value strings. + let arena = UnsyncArena::new(); + let mut arg_path = Vec::new(); + set_defaults_impl(&mut app, &mut arg_path, defaults, &arena); + let matches = app.get_matches_from(iter); + Self::from_clap(&matches) + } + + /// Set default values. Then gets the struct from any iterator such as a Vec of your making. + /// + /// Returns a clap::Error in case of failure. This does not exit in the case of --help or + /// --version, to achieve the same behavior as from_iter() you must call .exit() on the error + /// value. + fn try_from_iter_with_defaults( iter: I, defaults: &impl ConfigOptDefaults, ) -> ClapResult @@ -85,52 +114,33 @@ pub trait ConfigOpt: Sized + StructOpt { I: IntoIterator, I::Item: Into + Clone, { - from_iter_safe_with_defaults(iter, defaults) + let mut app = Self::clap(); + // An arena allocator is used to extend the lifetimes of the default value strings. + let arena = UnsyncArena::new(); + let mut arg_path = Vec::new(); + set_defaults_impl(&mut app, &mut arg_path, defaults, &arena); + let matches = app.get_matches_from_safe(iter)?; + Ok(Self::from_clap(&matches)) } - /// TODO - fn from_args_safe_ignore_help() -> ClapResult { - let args = env::args().filter(|a| a != "-h" && a != "--help"); - Self::from_iter_safe(args) + /// Gets the struct from the command line arguments taking into account `configopt` arguments. + /// Print the error message and quit the program in case of failure. + fn from_args() -> Self { + match Self::ConfigOptType::from_iter_safe(::std::env::args()) { + Ok(mut configopt) => { + configopt.maybe_generate_config_file_and_exit(); + match configopt.patch_with_config_files() { + Ok(configopt) => { + let mut s = Self::from_args_with_defaults(configopt); + ::take(&mut s, configopt); + s + } + Err(_) => todo!(), + } + } + Err(_) => todo!(), + } } -} - -/// Set the defaults for a `clap::App` -pub fn set_defaults(app: &mut App<'_, 'static>, defaults: &impl ConfigOptDefaults) { - let mut arg_path = Vec::new(); - let arena = &*DEFAULT_VALUE_STORE; - set_defaults_impl(app, &mut arg_path, defaults, arena); -} - -/// Construct an instance of a `structopt` struct using a set of defaults -pub fn from_args_with_defaults(defaults: &impl ConfigOptDefaults) -> T { - let mut app = T::clap(); - - // An arena allocator is used to extend the lifetimes of the default value strings. - let arena = UnsyncArena::new(); - let mut arg_path = Vec::new(); - set_defaults_impl(&mut app, &mut arg_path, defaults, &arena); - - let matches = app.get_matches(); - T::from_clap(&matches) -} - -/// Construct an instance of a `structopt` struct using a set of defaults -pub fn from_iter_safe_with_defaults( - iter: I, - defaults: &impl ConfigOptDefaults, -) -> ClapResult -where - I: IntoIterator, - I::Item: Into + Clone, -{ - let mut app = T::clap(); - - // An arena allocator is used to extend the lifetimes of the default value strings. - let arena = UnsyncArena::new(); - let mut arg_path = Vec::new(); - set_defaults_impl(&mut app, &mut arg_path, defaults, &arena); - let matches = app.get_matches_from_safe(iter)?; - Ok(T::from_clap(&matches)) + fn take(&mut self, configopt: &mut Self::ConfigOptType); } diff --git a/configopt/tests/test_all_types.rs b/configopt/tests/test_all_types.rs index 399c65a..cf8ba57 100644 --- a/configopt/tests/test_all_types.rs +++ b/configopt/tests/test_all_types.rs @@ -1,4 +1,4 @@ -use configopt::{configopt_fields, ConfigOpt, ConfigOptDefaults}; +use configopt::{configopt_fields, ConfigOpt, ConfigOptDefaults, ConfigOptType}; use serde::Deserialize; use std::{ffi::OsString, path::PathBuf}; use structopt::StructOpt; @@ -434,15 +434,15 @@ fn test_configopt_from_file_and_defaults() { ); assert_eq!( Some(OsString::from("from_cli_again2")), - s.arg_default(&[String::from("not_optional")]) + s.arg_default(&[String::from("notOptional")]) ); assert_eq!( Some(OsString::from("5.1")), - s.arg_default(&[String::from("double_optional")]) + s.arg_default(&[String::from("doubleOptional")]) ); assert_eq!( Some(OsString::from("4 5")), - s.arg_default(&[String::from("optional_vec")]) + s.arg_default(&[String::from("optionalVec")]) ); assert_eq!( Some(OsString::from("/this/is/a/path")), @@ -451,25 +451,45 @@ fn test_configopt_from_file_and_defaults() { assert_eq!( Some(OsString::from("from_cli_again3")), - s.arg_default(&[String::from("cmd3"), String::from("field_a")]) + s.arg_default(&[String::from("cmd3"), String::from("field-a")]) ); assert_eq!( Some(OsString::from("from_config4")), - s.arg_default(&[String::from("cmd3"), String::from("field_b")]) + s.arg_default(&[String::from("cmd3"), String::from("field-b")]) ); assert_eq!( Some(OsString::from("9")), - s.arg_default(&[String::from("cmd3"), String::from("flat_optional")]) + s.arg_default(&[String::from("cmd3"), String::from("flat-optional")]) ); assert_eq!( Some(OsString::from("true")), - s.arg_default(&[String::from("cmd3"), String::from("flat_maybe")]) + s.arg_default(&[String::from("cmd3"), String::from("flat-maybe")]) ); assert_eq!( Some(OsString::from("8 9 10")), - s.arg_default(&[String::from("cmd3"), String::from("flat_numbers")]) + s.arg_default(&[String::from("cmd3"), String::from("flat-numbers")]) ); } #[test] -fn test_configopt_toml() {} +fn test_setting_an_optional_field() { + use configopt::ConfigOpt; + + let mut c = ConfigOptMyStruct { + maybe: None, + numbers: Vec::new(), + optional: Some(String::from("configopt_optional")), + not_optional: Some(String::from("configopt_not_optional")), + double_optional: None, + optional_vec: None, + path: Some(PathBuf::from("/some/path")), + cmd: None, + config_files: Vec::new(), + generate_config: None, + }; + let mut s = + MyStruct::try_from_iter_with_defaults(&["app", "cmd3", "--field-a=from_cli"], &c).unwrap(); + assert!(s.optional.is_none()); + c.take_for(&mut s); + assert_eq!(Some(String::from("configopt_optional")), s.optional); +} diff --git a/configopt/tests/test_bool.rs b/configopt/tests/test_bool.rs new file mode 100644 index 0000000..da15c56 --- /dev/null +++ b/configopt/tests/test_bool.rs @@ -0,0 +1,96 @@ +use configopt::{configopt_fields, ConfigOpt}; +use serde::Deserialize; +use structopt::StructOpt; + +#[test] +fn test_configopt_with_optional_bool() { + #[configopt_fields] + #[derive(ConfigOpt, StructOpt, Debug, Deserialize, PartialEq)] + #[configopt(derive(Debug, PartialEq), attrs(serde))] + #[serde(deny_unknown_fields)] + struct MyStruct { + #[structopt(long, default_value = "true")] + maybe: Option, + } + + let c = ConfigOptMyStruct::from_iter_safe(&["app"]).unwrap(); + assert_eq!(None, c.maybe); + let c = ConfigOptMyStruct::from_iter_safe(&["app", "--maybe"]).unwrap(); + assert_eq!(Some(true), c.maybe); + let c = ConfigOptMyStruct::from_iter_safe(&["app", "--maybe=false"]).unwrap(); + assert_eq!(Some(false), c.maybe); + let c = ConfigOptMyStruct::from_iter_safe(&["app", "--maybe=true"]).unwrap(); + assert_eq!(Some(true), c.maybe); + + let c = ConfigOptMyStruct { + maybe: Some(true), + config_files: Vec::new(), + generate_config: None, + }; + let s = MyStruct::try_from_iter_with_defaults(&["app"], &c).unwrap(); + assert_eq!(None, s.maybe); + let s = MyStruct::try_from_iter_with_defaults(&["app", "--maybe"], &c).unwrap(); + assert_eq!(Some(true), s.maybe); + let s = MyStruct::try_from_iter_with_defaults(&["app", "--maybe=true"], &c).unwrap(); + assert_eq!(Some(true), s.maybe); + let s = MyStruct::try_from_iter_with_defaults(&["app", "--maybe=false"], &c).unwrap(); + assert_eq!(Some(false), s.maybe); + + let c = ConfigOptMyStruct { + maybe: Some(false), + config_files: Vec::new(), + generate_config: None, + }; + let s = MyStruct::try_from_iter_with_defaults(&["app"], &c).unwrap(); + assert_eq!(None, s.maybe); + // TODO: This is kinda odd. Because we set the default to false only passing the flag does not + // make the value true. We could probably use a `from_occurrences` parser to get around this. + let s = MyStruct::try_from_iter_with_defaults(&["app", "--maybe"], &c).unwrap(); + assert_eq!(Some(false), s.maybe); + let s = MyStruct::try_from_iter_with_defaults(&["app", "--maybe=true"], &c).unwrap(); + assert_eq!(Some(true), s.maybe); + let s = MyStruct::try_from_iter_with_defaults(&["app", "--maybe=false"], &c).unwrap(); + assert_eq!(Some(false), s.maybe); +} + +#[test] +fn test_configopt_with_bool() { + #[configopt_fields] + #[derive(ConfigOpt, StructOpt, Debug, Deserialize, PartialEq)] + #[configopt(derive(Debug, PartialEq), attrs(serde))] + #[serde(deny_unknown_fields)] + struct MyStruct { + #[structopt(long)] + maybe: bool, + } + + let c = ConfigOptMyStruct::from_iter_safe(&["app"]).unwrap(); + assert_eq!(None, c.maybe); + let c = ConfigOptMyStruct::from_iter_safe(&["app", "--maybe"]).unwrap(); + assert_eq!(Some(true), c.maybe); + let c = ConfigOptMyStruct::from_iter_safe(&["app", "--maybe=false"]).unwrap(); + assert_eq!(Some(false), c.maybe); + let c = ConfigOptMyStruct::from_iter_safe(&["app", "--maybe=true"]).unwrap(); + assert_eq!(Some(true), c.maybe); + + let c = ConfigOptMyStruct { + maybe: Some(true), + config_files: Vec::new(), + generate_config: None, + }; + let s = MyStruct::try_from_iter_with_defaults(&["app"], &c).unwrap(); + // We want this to be true, but setting a default value for a boolean is impossible. + assert_eq!(false, s.maybe); + let s = MyStruct::try_from_iter_with_defaults(&["app", "--maybe"], &c).unwrap(); + assert_eq!(true, s.maybe); + + let c = ConfigOptMyStruct { + maybe: Some(false), + config_files: Vec::new(), + generate_config: None, + }; + let s = MyStruct::try_from_iter_with_defaults(&["app"], &c).unwrap(); + assert_eq!(false, s.maybe); + let s = MyStruct::try_from_iter_with_defaults(&["app", "--maybe"], &c).unwrap(); + assert_eq!(true, s.maybe); +}