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
//! This module contains the configuration of the application.
//!
//! All options are passed individually to each function and are not bundled together.
//!
//! # Examples
//!
//! ```no_run
//! # use tweetr::options::Options;
//! let options = Options::parse();
//! println!("Config directory: {}", options.config_dir.0);
//! ```


use clap::{self, App, SubCommand, Arg, AppSettings};
use std::time::Duration;
use std::path::PathBuf;
use std::env::home_dir;
use std::str::FromStr;
use std::fs;


/// All possible subsystems, think `cargo`'s or `git`'s.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Subsystem {
    /// Initialise global app data
    Init {
        /// Whether to override current app configuration. Default: `false`
        force: bool,
    },
    /// Add and authorise a user
    AddUser {
        /// Whether to print more user data. Default: `false`
        verbose: bool,
    },
    /// Add a tweet to the queue
    QueueTweet {
        /// File to load tweets from, if any. Default: `None`
        file_to_load: Option<PathBuf>,
    },
    /// Start the tweet-posting daemon.
    StartDaemon {
        /// How long to wait between trying to post again. Default: 60s
        delay: Duration,
        /// Whether to log all network requests. Default: `false`
        verbose: bool,
    },
}


/// Representation of the application's all configurable values.
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct Options {
    /// Directory containing configuration. Default: `"$HOME/.tweetr"`
    pub config_dir: (String, PathBuf),
    /// The specified subsystem.
    pub subsystem: Subsystem,
}

impl Options {
    /// Parse `env`-wide command-line arguments into an `Options` instance
    pub fn parse() -> Options {
        let matches = App::new("tweetr")
            .version(crate_version!())
            .author(crate_authors!())
            .setting(AppSettings::ColoredHelp)
            .setting(AppSettings::VersionlessSubcommands)
            .setting(AppSettings::SubcommandRequiredElseHelp)
            .about("tweetr is a platform that allows you to create and queue tweets to be shared when YOU want.\n\
                    You create content when you have time  and then use FOSS and NOT pay whatever-ridiculous\n\
                    amount of $$$ for posting them automatically")
            .arg(Arg::from_usage("-c --config-dir=[CONFIG_DIR] 'Directory containing configuration. Default: $HOME/.tweetr'")
                .validator(Options::config_dir_validator))
            .subcommand(SubCommand::with_name("init")
                .about("Initialise global app data")
                .arg(Arg::from_usage("-f --force 'Override current app configuration'")))
            .subcommand(SubCommand::with_name("add-user")
                .about("Add and authorise a user")
                .arg(Arg::from_usage("-v --verbose 'Print more user data'")))
            .subcommand(SubCommand::with_name("queue-tweet")
                .about("Add a tweet to the queue")
                .arg(Arg::from_usage("-f --file=[file] 'Load tweets from the specified file'").validator(Options::tweets_file_validator)))
            .subcommand(SubCommand::with_name("start-daemon")
                .about("Start the tweet-posting daemon")
                .args(&[Arg::from_usage("-v --verbose 'Log all network requests'"),
                        Arg::from_usage("--delay=<delay> 'How long to wait between trying to post again [ms]'")
                            .default_value("60000")
                            .validator(Options::duration_validator)]))
            .get_matches();

        Options {
            config_dir: match matches.value_of("config-dir") {
                Some(dirs) => (dirs.to_string(), fs::canonicalize(dirs).unwrap()),
                None => {
                    match home_dir() {
                        Some(mut hd) => {
                            hd = hd.canonicalize().unwrap();
                            hd.push(".tweetr");

                            fs::create_dir_all(&hd).unwrap();
                            ("$HOME/.tweetr".to_string(), hd)
                        }
                        None => {
                            clap::Error {
                                    message: "Couldn't automatically get home directory, please specify configuration directory with the -c option".to_string(),
                                    kind: clap::ErrorKind::MissingRequiredArgument,
                                    info: None,
                                }
                                .exit()
                        }
                    }
                }
            },
            subsystem: match matches.subcommand() {
                ("init", Some(init_matches)) => Subsystem::Init { force: init_matches.is_present("force") },
                ("add-user", Some(add_user_matches)) => Subsystem::AddUser { verbose: add_user_matches.is_present("verbose") },
                ("queue-tweet", Some(queue_tweet_matches)) => {
                    Subsystem::QueueTweet { file_to_load: queue_tweet_matches.value_of("file").map(fs::canonicalize).map(Result::unwrap) }
                }
                ("start-daemon", Some(start_daemon_matches)) => {
                    Subsystem::StartDaemon {
                        delay: Duration::from_millis(u64::from_str(start_daemon_matches.value_of("delay").unwrap()).unwrap()),
                        verbose: start_daemon_matches.is_present("verbose"),
                    }
                }
                _ => panic!("No subcommand passed"),
            },
        }
    }

    fn config_dir_validator(s: String) -> Result<(), String> {
        fs::canonicalize(&s).map(|_| ()).map_err(|_| format!("Configuration directory \"{}\" not found", s))
    }

    fn tweets_file_validator(s: String) -> Result<(), String> {
        fs::canonicalize(&s).map(|_| ()).map_err(|_| format!("File with tweets \"{}\" not found", s))
    }

    fn duration_validator(s: String) -> Result<(), String> {
        u64::from_str(&s).map(|_| ()).map_err(|_| format!("\"{}\" is not a valid amount of milliseconds", s))
    }
}