1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//! StructConf is a derive macro to combine argument parsing from
//! [clap](https://docs.rs/clap/) and config file parsing from [rust-ini](
//! https://docs.rs/rust-ini/) at compile time. For example:
//!
//! ```rust
//! use clap::App;
//! use structconf::StructConf;
//!
//! #[derive(Debug, StructConf)]
//! struct Config {
//!     // Option available in the config file and the arguments
//!     #[conf(help = "description for the argument parser.")]
//!     pub default: i32,
//!     // Specify where the options are available.
//!     #[conf(no_file)]
//!     pub args_opt: u8,
//!     #[conf(no_short, no_long)]
//!     pub conf_opt: Option<String>,
//!     #[conf(no_short, no_long, no_file)]
//!     pub ignored: bool,
//!     // Customize the names
//!     #[conf(short = "x", long = "renamed_opt", file = "my_opt",
//!            help = "custom names.")]
//!     pub renamed: String,
//!     // Inverse arguments
//!     #[conf(short = "n", long = "no_pancakes", help = "disable pancakes.")]
//!     pub pancakes: bool,
//!     // Custom default values
//!     #[conf(default = "123.45")]
//!     pub floating: f64,
//! }
//!
//! pub fn main() {
//!     let app = App::new("demo");
//!     let conf = Config::parse(app, "config.ini");
//!     println!("Parsed config: {:#?}", conf);
//! }
//! ```
//!
//! Any named struct that uses `#[derive(StructConf)]` will have the methods
//! from [`structconf::StructConf`](
//! https://docs.rs/structconf/latest/structconf/trait.StructConf.html)
//! available.
//!
//! Additional attributes can be added to its fields to customize how they
//! are parsed:
//!
//! ## General attributes
//! * `default = "..."`: a Rust expression that will be evaluated as a
//! fallback value. For example, `default = "1+2"`, or
//! `default = "String::from(\"hello\"")`. Otherwise, the value given by
//! [`std::default::Default`](
//! https://doc.rust-lang.org/std/default/trait.Default.html) will be used,
//! or in case the assigned type is `Option<T>`\*, `None`.
//!
//! \* *Note: the assigned type must be exactly `Option<T>` for this to work.
//! `std::option::Option<T>` won't work, for example.*
//!
//! ## Argument parser attributes
//! * `help = "..."`: the help message shown in the argument parser when
//! `--help` is used.
//! * `long = "arg_name"`: a custom long argument name. Otherwise, it will be
//! obtained directly from the field's name. `do_something` will be
//! `--do-something`.
//! * `no_long`: don't include the option as a long argument.
//! * `short = "x"`: a custom short argument name (only made up of a single
//! character). Otherwise, it will be obtained directly from the field's
//! name. `do_something` will be `-d`.
//! * `no_short`: don't include the option as a short argument.
//! * `inverse_arg`: the argument value is the opposite. The assigned type
//! must implement [`std::ops::Not`](
//! https://doc.rust-lang.org/std/ops/trait.Not.html). This is useful for
//! arguments where the argument's value is negated:
//!
//! ```rust
//! use structconf::StructConf;
//!
//! #[derive(StructConf)]
//! struct Bakery {
//!     #[conf(inverse_arg, no_short, long = "--no-pancakes")]
//!     pancakes: bool
//! }
//! ```
//!
//! If both `no_long` and `no_short` are provided, the option won't be
//! available in the argument parser at all.
//!
//! ## Config file attributes
//! * `file = "..."`: a custom option name for the config file. Otherwise, it
//! will be the same as the field's name.
//! * `no_file`: son't include the option in the config file.
//! * `section`: the section in the config file where the option will be.
//! Otherwise, `Default` is used. For example,
//! `#[structconf(section = "Planes")] model_id: i32` will look like this in
//! the config file:
//!
//! ```ini
//! [Planes]
//! model_id = 123
//! ```

pub use structconf_derive::StructConf;

use std::ffi::OsString;
use std::fmt;
use std::io;

/// Small wrapper for the possible errors that may occur when parsing a
/// StructConf-derived struct.
#[derive(Debug)]
pub enum Error {
    IO(io::Error),
    Ini(ini::ini::ParseError),
    Parse(String),
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match &self {
            Error::IO(err) => err.fmt(f),
            Error::Ini(err) => err.fmt(f),
            Error::Parse(msg) => write!(f, "Error when parsing: {}", msg),
        }
    }
}

impl From<io::Error> for Error {
    fn from(err: io::Error) -> Self {
        Error::IO(err)
    }
}

impl From<ini::ini::Error> for Error {
    fn from(err: ini::ini::Error) -> Self {
        match err {
            ini::ini::Error::Io(err) => Error::IO(err),
            ini::ini::Error::Parse(err) => Error::Ini(err),
        }
    }
}

/// This trait implements the methods available after using
/// `#[derive(StructConf)]`.
///
/// The priority followed for the configuration is "arguments > config file >
/// default values".
pub trait StructConf {
    /// Instantiate the structure from both the argument parser and the
    /// config file, falling back to the default values. Equivalent to
    /// calling `parse_args` and then `parse_file`.
    ///
    /// The `path` argument is where the config file will be. If it doesn't
    /// exist, it will be created, and a message to stderr will be printed.
    fn parse(app: clap::App, path: &str) -> Result<Self, Error>
    where
        Self: Sized;

    /// Parses only the arguments with [clap](
    /// https://docs.rs/clap/2.33.1/clap/). This is useful for a
    /// `--config-file` argument to allow the user to choose the config
    /// file location.
    ///
    /// This is equivalent to `parse_args_from(..., &mut std::env::args())`.
    fn parse_args<'a>(app: clap::App<'a, 'a>) -> clap::ArgMatches<'a>;

    /// Parses only the arguments with [clap](
    /// https://docs.rs/clap/2.33.1/clap/) from an iterator.
    fn parse_args_from<'a, I, T>(
        app: clap::App<'a, 'a>,
        iter: I,
    ) -> clap::ArgMatches<'a>
    where
        I: IntoIterator<Item = T>,
        T: Into<OsString> + Clone;

    /// The config file is read after parsing the arguments, and the struct
    /// is initialized with the default values taken into account.
    ///
    /// The `path` argument is where the config file will be. If it doesn't
    /// exist, it will be created, and a message to stderr will be printed.
    ///
    /// This also serves as a function to refresh the config file values.
    fn parse_file(args: &clap::ArgMatches, path: &str) -> Result<Self, Error>
    where
        Self: Sized;

    /// Writes the structure's values into a config file, except for those
    /// that are wrapped by `Option` and whose value is `None`.
    fn write_file(&self, path: &str) -> Result<(), Error>;
}