rustfoundry/cli.rs
1//! Command line interface-related functionality.
2
3use super::settings::Settings;
4use super::{BootstrapResult, ServiceInfo};
5use anyhow::anyhow;
6use clap::error::ErrorKind;
7use clap::Command;
8use std::ffi::OsString;
9
10pub use clap::{Arg, ArgAction, ArgMatches};
11
12const GENERATE_CONFIG_OPT_ID: &str = "generate";
13const USE_CONFIG_OPT_ID: &str = "config";
14
15/// A command line interface (CLI) helper that takes care of the command line arguments parsing
16/// basics.
17///
18/// Rustfoundry-based services are expected to primarily use [`Settings`] for its configuration. This
19/// helper takes care of setting up CLI with the provided [`ServiceInfo`] and service configuration
20/// parsing.
21///
22/// By default the following command line options are added:
23///
24/// - `-c`, `--config` - specifies an existing configuration file for the service.
25/// - `-g`, `--generate` - generates a new default configuration file for the service.
26/// - `-h`, `--help` - prints CLI help information and exits.
27/// - `-v`, `--version` - prints the service version and exits.
28///
29/// Additional arguments can be added via `custom_args` argument of the [`Cli::new`] function.
30///
31/// [`Settings`]: crate::settings::Settings
32pub struct Cli<S: Settings> {
33 /// Parsed service settings.
34 pub settings: S,
35
36 /// Parsed service arguments.
37 pub arg_matches: ArgMatches,
38}
39
40impl<S: Settings> Cli<S> {
41 /// Bootstraps a new command line interface (CLI) for the service.
42 ///
43 /// `custom_args` argument can be used to add extra service-specific arguments to the CLI.
44 ///
45 /// The function will implicitly print relevant information and exit the process if
46 /// `--help` or `--version` command line options are specified.
47 ///
48 /// Any command line parsing errors are intentionally propagated as a [`BootstrapResult`],
49 /// so they can be reported to a panic handler (e.g. [Sentry]) if the service uses one.
50 ///
51 /// [Sentry]: https://sentry.io/
52 pub fn new(service_info: &ServiceInfo, custom_args: Vec<Arg>) -> BootstrapResult<Self> {
53 Self::new_from_os_args(service_info, custom_args, std::env::args_os())
54 }
55
56 /// Bootstraps a new command line interface (CLI) for the service with the provided `os_args`.
57 ///
58 /// This method is the same as [`Cli::new`], but accepts source OS arguments instead of taking
59 /// them fron [`std::env::args_os`].
60 ///
61 /// Useful for testing purposes.
62 pub fn new_from_os_args(
63 service_info: &ServiceInfo,
64 custom_args: Vec<Arg>,
65 os_args: impl IntoIterator<Item = impl Into<OsString> + Clone>,
66 ) -> BootstrapResult<Self> {
67 let mut cmd = Command::new(service_info.name)
68 .version(service_info.version)
69 .author(service_info.author)
70 .about(service_info.description)
71 .arg(
72 Arg::new("config")
73 .required_unless_present(GENERATE_CONFIG_OPT_ID)
74 .action(ArgAction::Set)
75 .long("config")
76 .short('c')
77 .help("Specifies the config to run the service with"),
78 )
79 .arg(
80 Arg::new(GENERATE_CONFIG_OPT_ID)
81 .action(ArgAction::Set)
82 .long("generate")
83 .short('g')
84 .help("Generates a new default config for the service"),
85 );
86
87 for arg in custom_args {
88 cmd = cmd.arg(arg);
89 }
90
91 let arg_matches = get_arg_matches(cmd, os_args)?;
92 let settings = get_settings(&arg_matches)?;
93
94 Ok(Self {
95 settings,
96 arg_matches,
97 })
98 }
99}
100
101fn get_arg_matches(
102 cmd: Command,
103 os_args: impl IntoIterator<Item = impl Into<OsString> + Clone>,
104) -> BootstrapResult<ArgMatches> {
105 cmd.try_get_matches_from(os_args).map_err(|e| {
106 let kind = e.kind();
107
108 // NOTE: print info and terminate the process
109 if kind == ErrorKind::DisplayHelp || kind == ErrorKind::DisplayVersion {
110 e.exit();
111 }
112
113 // NOTE: otherwise propagate as an error, so it can be captured by a panic handler
114 // if necesary (e.g. Sentry).
115 e.into()
116 })
117}
118
119fn get_settings<S: Settings>(arg_matches: &ArgMatches) -> BootstrapResult<S> {
120 if let Some(path) = arg_matches.get_one::<String>(GENERATE_CONFIG_OPT_ID) {
121 let settings = S::default();
122
123 crate::settings::to_yaml_file(&settings, path)?;
124
125 return Ok(settings);
126 }
127
128 if let Some(path) = arg_matches.get_one::<String>(USE_CONFIG_OPT_ID) {
129 return crate::settings::from_file(path).map_err(|e| anyhow!(e));
130 }
131
132 unreachable!("clap should require config options to be present")
133}