clap_config_fallback/
lib.rs1mod arg;
11pub mod format;
12
13use std::{
14 ffi::{OsStr, OsString},
15 fs, iter,
16 path::{Path, PathBuf},
17};
18
19use clap::{CommandFactory, Error, Parser, error::ErrorKind};
20use figment::{Figment, providers::*};
21use serde::{Serialize, de::DeserializeOwned};
22
23#[cfg(feature = "derive")]
24pub use clap_config_fallback_derive::{ConfigArgs, ConfigParser, ConfigSubcommand};
25
26pub use arg::{Arg, FromArgs, IntoArgs};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ConfigFormat {
31 Toml,
32 Yaml,
33 Json,
34}
35
36pub trait ConfigSource {
38 fn config_path(&self) -> Option<&Path>;
40
41 fn config_format(&self) -> Option<ConfigFormat> {
48 self.config_path()
49 .and_then(|path| match path.extension().and_then(OsStr::to_str) {
50 #[cfg(feature = "toml")]
51 Some("toml") => Some(ConfigFormat::Toml),
52 #[cfg(feature = "yaml")]
53 Some("yaml" | "yml") => Some(ConfigFormat::Yaml),
54 #[cfg(feature = "json")]
55 Some("json") => Some(ConfigFormat::Json),
56 _ => None,
57 })
58 }
59}
60
61pub trait ConfigFallback {
63 type Opts: Serialize + DeserializeOwned + IntoArgs + FromArgs;
65 type Config: Serialize + DeserializeOwned;
67}
68
69pub trait ConfigParser: Sized + Parser + ConfigFallback
74where
75 Self::Opts: Parser + Default + ConfigSource,
76{
77 fn parse_with_config() -> Self {
79 Self::parse_with_config_from(std::env::args_os())
80 }
81
82 fn try_parse_with_config() -> Result<Self, Error> {
84 Self::try_parse_with_config_from(std::env::args_os())
85 }
86
87 fn parse_with_config_from<I, T>(itr: I) -> Self
89 where
90 I: IntoIterator<Item = T>,
91 T: Into<OsString> + Clone,
92 {
93 Self::try_parse_with_config_from(itr).unwrap_or_else(|e| e.exit())
94 }
95
96 fn try_parse_with_config_from<I, T>(args: I) -> Result<Self, Error>
101 where
102 I: IntoIterator<Item = T>,
103 T: Into<OsString> + Clone,
104 {
105 let command = <Self::Opts as CommandFactory>::command();
106 let command_name = command.get_name().to_owned().into();
107 let args = args.into_iter().collect::<Vec<_>>();
108
109 for arg in args.iter().cloned() {
110 if let Some("--help" | "-h" | "--version" | "-V") = arg.into().as_os_str().to_str() {
112 return Self::try_parse_from(args);
113 }
114 }
115
116 let args = command.try_get_matches_from(args)?;
117 let opts = Self::Opts::from_args(&args).unwrap_or_default();
118
119 let config: Option<Self::Config> = opts
120 .config_path()
121 .map(|path| {
122 let path = PathBuf::from(path);
123
124 if !path.exists() {
125 return Err(
126 Self::command().error(ErrorKind::Io, "configuration file not found")
127 );
128 }
129
130 let content = fs::read_to_string(path).map_err(|e| {
131 Self::command().error(
132 ErrorKind::Io,
133 format!("failed to read configuration file: {e}"),
134 )
135 })?;
136
137 match opts.config_format() {
138 #[cfg(feature = "toml")]
139 Some(ConfigFormat::Toml) => toml::from_str(&content).map_err(|e| e.to_string()),
140 #[cfg(feature = "yaml")]
141 Some(ConfigFormat::Yaml) => {
142 noyalib::from_str_borrowing(&content).map_err(|e| e.to_string())
143 }
144 #[cfg(feature = "json")]
145 Some(ConfigFormat::Json) => {
146 serde_json::from_str(&content).map_err(|e| e.to_string())
147 }
148 _ => Err("unsupported format".to_string()),
149 }
150 .map_err(|e| {
151 Self::command().error(
152 ErrorKind::InvalidValue,
153 format!("invalid configuration file: {e}"),
154 )
155 })
156 })
157 .transpose()?;
158
159 let mut cli = Figment::from(Serialized::defaults(opts));
160
161 if let Some(config) = config {
162 cli = cli.join(Serialized::defaults(config));
163 }
164
165 let cli = cli
166 .extract::<Self::Opts>()
167 .map_err(|e| Self::command().error(ErrorKind::InvalidValue, e.to_string()))?;
168
169 Self::try_parse_from(iter::once(command_name).chain(cli.into_args()))
170 }
171}