blob: 31e6801ee231f0e1af76ddf0e058fb7670430728 [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use std::marker::PhantomData;
use std::process::exit;
use getopts::{self, Matches, Options};
use thiserror::Error as ThisError;
use crate::transport::{self, TransportType, LOOPBACK_DEFAULT};
#[derive(ThisError, Debug)]
pub enum Error {
#[error("failed to parse transport type: {0}")]
TransportParse(transport::Error),
}
/// The result of an operation in this crate.
pub type Result<T> = std::result::Result<T, Error>;
const DEFAULT_TRANSPORT_TYPE_SHORT_NAME: &str = "U";
const DEFAULT_TRANSPORT_TYPE_LONG_NAME: &str = "server-url";
const DEFAULT_TRANSPORT_TYPE_DESC: &str = "URL to the server";
const HELP_OPTION_SHORT_NAME: &str = "h";
/// A TransportType command-line option that allows for multiple transport options to be defined for
/// the same set of arguments.
pub struct TransportTypeOption {
short_name: String,
}
impl TransportTypeOption {
/// Add the default TransportTypeOption to the specified Options.
pub fn default(mut opts: &mut Options) -> Self {
Self::new(
DEFAULT_TRANSPORT_TYPE_SHORT_NAME,
DEFAULT_TRANSPORT_TYPE_LONG_NAME,
DEFAULT_TRANSPORT_TYPE_DESC,
LOOPBACK_DEFAULT,
&mut opts,
)
}
/// Add a customized TransportTypeOption to the specified Options.
/// Prefer Self::default unless it has already been used.
pub fn new(
short_name: &str,
long_name: &str,
desc: &str,
hint: &str,
opts: &mut Options,
) -> Self {
opts.optflagopt(short_name, long_name, desc, hint);
TransportTypeOption {
short_name: short_name.to_string(),
}
}
/// Checks the command line argument matches and returns:
/// * Ok(None) - if the option wasn't set.
/// * Ok(Some(_: TransportType)) - if parsing succeeded.
/// * Err(Error::TransportParse(_)) - if parsing failed.
pub fn from_matches(&self, matches: &Matches) -> Result<Option<TransportType>> {
match matches.opt_str(&self.short_name) {
Some(value) => value
.parse::<TransportType>()
.map_err(Error::TransportParse)
.map(Some),
None => Ok(None),
}
}
}
trait PrivateTrait {}
/// A help option that wraps commonly used logic. Specifically, it defines the "-h", "--help"
/// options and provides a helper for showing the usage strings when the option is set or when
/// the command line options fail to parse.
pub struct HelpOption {
// Force use of the constructor through a private field.
phantom: PhantomData<dyn PrivateTrait>,
}
impl HelpOption {
/// Adds a newly created HelpOption to the specified Options.
pub fn new(opts: &mut Options) -> Self {
let help = HelpOption {
phantom: PhantomData,
};
opts.optflag(help.get_short_name(), "help", "Show this help string.");
help
}
/// Return the short_name used by the HelpOption for direct use with Options.
pub fn get_short_name(&self) -> &str {
HELP_OPTION_SHORT_NAME
}
/// Mocks process::exit for testability. See: parse_and_check_self
fn parse_and_check_self_impl<F: Fn(i32) -> ()>(
&self,
opts: &Options,
args: &[String],
get_usage: fn() -> String,
exit_fn: F,
) -> Option<Matches> {
let matches = opts.parse(args).map(Some).unwrap_or_else(|e| {
eprintln!("{}", e);
println!("{}", opts.usage(&get_usage()));
exit_fn(1);
None
})?;
if matches.opt_present(self.get_short_name()) {
println!("{}", opts.usage(&get_usage()));
exit_fn(0);
}
Some(matches)
}
/// A wrapper around Options::parse that handles printing the help string on a parsing error or
/// when the help option is set.
pub fn parse_and_check_self(
&self,
opts: &Options,
args: &[String],
get_usage: fn() -> String,
) -> Matches {
self.parse_and_check_self_impl(opts, args, get_usage, |x| exit(x))
.unwrap()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use std::rc::Rc;
fn test_usage() -> String {
"test usage string".to_string()
}
fn get_counted_mock_exit(expected: i32) -> (Rc<RefCell<usize>>, impl Fn(i32)) {
let counter = Rc::new(RefCell::new(0 as usize));
(counter.clone(), move |code: i32| {
assert_eq!(code, expected);
*counter.borrow_mut() += 1;
})
}
#[test]
fn transporttypeoption_frommatches_notset() {
let test_args: Vec<String> = Vec::new();
let mut opts = Options::new();
let url_option = TransportTypeOption::default(&mut opts);
let matches = opts.parse(&test_args).unwrap();
assert!(matches!(url_option.from_matches(&matches), Ok(None)));
}
#[test]
fn transporttypeoption_frommatches_valid() {
let test_args: Vec<String> = vec!["-U".to_string(), LOOPBACK_DEFAULT.to_string()];
let mut opts = Options::new();
let url_option = TransportTypeOption::default(&mut opts);
let matches = opts.parse(&test_args).unwrap();
assert!(matches!(url_option.from_matches(&matches), Ok(Some(_))));
}
#[test]
fn transporttypeoption_frommatches_notvalid() {
let test_args: Vec<String> =
vec!["-U".to_string(), "not a valid transport type".to_string()];
let mut opts = Options::new();
let url_option = TransportTypeOption::default(&mut opts);
let matches = opts.parse(&test_args).unwrap();
assert!(matches!(
url_option.from_matches(&matches),
Err(Error::TransportParse(_))
));
}
#[test]
fn helpoption_parseandcheckself_notset() {
let test_args: Vec<String> = Vec::new();
let mut opts = Options::new();
let help_option = HelpOption::new(&mut opts);
help_option.parse_and_check_self_impl(&opts, &test_args, test_usage, |_| panic!());
}
#[test]
fn helpoption_parseandcheckself_set() {
let test_args: Vec<String> = vec!["-h".to_string()];
let (counter, exit_fn) = get_counted_mock_exit(0);
let mut opts = Options::new();
let help_option = HelpOption::new(&mut opts);
let matches =
help_option.parse_and_check_self_impl(&opts, &test_args, test_usage, |x| exit_fn(x));
assert!(matches.is_some());
assert_eq!(*counter.borrow(), 1);
}
#[test]
fn helpoption_parseandcheckself_invalid() {
let test_args: Vec<String> = vec!["--not-an-option".to_string()];
let (counter, exit_fn) = get_counted_mock_exit(1);
let mut opts = Options::new();
let help_option = HelpOption::new(&mut opts);
let matches =
help_option.parse_and_check_self_impl(&opts, &test_args, test_usage, |x| exit_fn(x));
assert!(matches.is_none());
assert_eq!(*counter.borrow(), 1);
}
}