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
//! 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. Here's a very simple example (see
//! more detailed examples on [the GitHub repo](
//! https://github.com/marioortizmanero/structconf/tree/master/examples)):
//!
//! ```rust
//! use structconf::{clap, StructConf};
//!
//! #[derive(Debug, StructConf)]
//! struct ServerConfig {
//!     #[conf(help = "The public key")]
//!     pub public_key: String,
//!     #[conf(no_file, long = "your-secret", help = "Your secret API key")]
//!     pub secret_key: String,
//!     #[conf(default = "100", help = "timeout in seconds")]
//!     pub timeout: i32,
//! }
//!
//! let app = clap::App::new("demo");
//! let conf = ServerConfig::parse(app, "config.ini");
//! ```
//!
//! Any named struct that uses `#[derive(StructConf)]` will have the methods
//! from [`structconf::StructConf`](
//! https://docs.rs/structconf/latest/structconf/trait.StructConf.html)
//! available.
//!
//! You can access the `clap` and `ini` crates inside `structconf::clap` and
//! `structconf::ini` to avoid duplicate dependencies and not having to include
//! them in your `Cargo.toml`.
//!
//! 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.
//! * `negated_arg`: the flag's value is the opposite:
//!
//! ```rust
//! use structconf::StructConf;
//!
//! #[derive(StructConf)]
//! struct Bakery {
//!     // By default it's `true`, unless `--no-pancakes` is passed.
//!     #[conf(negated_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 = "..."`: set a custom name in the config file. Otherwise, it will
//! be the same as the field's identifier.
//! * `no_file`: don'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
//! ```

/// Re-exporting the `clap` module used in the macro.
pub use clap;
/// Re-exporting the `ini` module used in the macro.
pub use ini;
pub use structconf_derive::StructConf;

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

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

impl From<ini::Error> for Error {
    fn from(err: ini::Error) -> Self {
        match err {
            ini::Error::Io(err) => Error::IO(err),
            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(app: clap::App) -> clap::ArgMatches;

    /// Parses only the arguments with [clap](
    /// https://docs.rs/clap/2.33.1/clap/) from an iterator.
    fn parse_args_from<I, T>(app: clap::App, iter: I) -> clap::ArgMatches
    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>;
}