1use std::{collections::HashSet, ffi::OsString, path::PathBuf};
4
5pub use clap::{ArgMatches, Command};
6pub use config::Config as OroConfig;
7use config::{builder::DefaultState, ConfigBuilder, Environment, File, ValueKind};
8use kdl_source::KdlFormat;
9use miette::Result;
10
11use error::OroConfigError;
12
13mod error;
14mod kdl_source;
15
16pub trait OroConfigLayerExt {
17 fn with_negations(self) -> Self;
18 fn layered_args(&self, args: &mut Vec<OsString>, config: &OroConfig) -> Result<()>;
19}
20
21impl OroConfigLayerExt for Command {
22 fn with_negations(self) -> Self {
23 let negated = self
24 .get_arguments()
25 .filter(|opt| opt.get_long().is_some())
26 .map(|opt| format!("no-{}", opt.get_long().expect("long option")))
27 .collect::<Vec<_>>();
28 let negations = self
29 .get_arguments()
30 .filter(|opt| opt.get_long().is_some())
31 .zip(negated)
32 .map(|(opt, negated)| {
33 let long = if negated.starts_with("no-no-") {
38 negated.replace("no-no-", "")
39 } else {
40 negated.clone()
41 };
42 clap::Arg::new(negated)
43 .long(long)
44 .global(opt.is_global_set())
45 .hide(true)
46 .action(clap::ArgAction::SetTrue)
47 .overrides_with(opt.get_id())
48 })
49 .collect::<Vec<_>>();
50 self.args(negations)
52 }
53
54 fn layered_args(&self, args: &mut Vec<OsString>, config: &OroConfig) -> Result<()> {
55 let mut long_opts = HashSet::new();
56 for opt in self.get_arguments() {
57 if opt.get_long().is_some() {
58 long_opts.insert(opt.get_id().to_string());
59 }
60 }
61 let matches = self
62 .clone()
63 .ignore_errors(true)
64 .get_matches_from(&args.clone());
65 for opt in long_opts {
66 if matches.value_source(&opt) != Some(clap::parser::ValueSource::CommandLine) {
71 let opt = opt.replace('_', "-");
72 if !args.contains(&OsString::from(format!("--no-{opt}"))) {
73 if let Ok(bool) = config.get_bool(&opt) {
74 if bool {
75 args.push(OsString::from(format!("--{}", opt)));
76 } else {
77 args.push(OsString::from(format!("--no-{}", opt)));
78 }
79 } else if let Ok(value) = config.get_string(&opt) {
80 args.push(OsString::from(format!("--{}", opt)));
81 args.push(OsString::from(value));
82 } else if let Ok(value) = config.get_table(&opt) {
83 for (key, val) in value {
84 match &val.kind {
85 ValueKind::Table(map) => {
86 for (k, v) in map {
87 args.push(OsString::from(format!("--{}", opt)));
88 args.push(OsString::from(format!("{{{key}}}{k}={v}")));
89 }
90 }
91 _ => {
93 args.push(OsString::from(format!("--{}", opt)));
94 args.push(OsString::from(format!("{key}={val}")));
95 }
96 }
97 }
98 } else if let Ok(value) = config.get_array(&opt) {
99 for val in value {
100 if let Ok(val) = val.into_string() {
101 args.push(OsString::from(format!("--{}", opt)));
102 args.push(OsString::from(val));
103 }
104 }
105 }
106 }
107 }
108 }
109 Ok(())
110 }
111}
112
113#[derive(Debug, Clone)]
114pub struct OroConfigOptions {
115 builder: ConfigBuilder<DefaultState>,
116 global: bool,
117 env: bool,
118 pkg_root: Option<PathBuf>,
119 global_config_file: Option<PathBuf>,
120}
121
122impl Default for OroConfigOptions {
123 fn default() -> Self {
124 OroConfigOptions {
125 builder: OroConfig::builder(),
126 global: true,
127 env: true,
128 pkg_root: None,
129 global_config_file: None,
130 }
131 }
132}
133
134impl OroConfigOptions {
135 pub fn new() -> Self {
136 Self::default()
137 }
138
139 pub fn global(mut self, global: bool) -> Self {
140 self.global = global;
141 self
142 }
143
144 pub fn env(mut self, env: bool) -> Self {
145 self.env = env;
146 self
147 }
148
149 pub fn pkg_root(mut self, root: Option<PathBuf>) -> Self {
150 self.pkg_root = root;
151 self
152 }
153
154 pub fn global_config_file(mut self, file: Option<PathBuf>) -> Self {
155 self.global_config_file = file;
156 self
157 }
158
159 pub fn set_default(mut self, key: &str, value: &str) -> Result<Self, OroConfigError> {
160 self.builder = self.builder.set_default(key, value)?;
161 Ok(self)
162 }
163
164 pub fn load(self) -> Result<OroConfig> {
165 let mut builder = self.builder;
166 if self.global {
167 if let Some(config_file) = self.global_config_file {
168 let path = config_file.display().to_string();
169 builder = builder.add_source(File::new(&path, KdlFormat).required(false));
170 }
171 }
172 if self.env {
173 builder = builder.add_source(Environment::with_prefix("oro_config"));
174 }
175 if let Some(root) = self.pkg_root {
176 builder = builder.add_source(
177 File::new(&root.join("oro.kdl").display().to_string(), KdlFormat).required(false),
178 );
179 }
180 Ok(builder.build().map_err(OroConfigError::ConfigError)?)
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 use std::env;
189 use std::fs;
190
191 use miette::{IntoDiagnostic, Result};
192 use pretty_assertions::assert_eq;
193 use tempfile::tempdir;
194
195 #[test]
196 fn env_configs() -> Result<()> {
197 let dir = tempdir().into_diagnostic()?;
198 env::set_var("ORO_CONFIG_STORE", dir.path().display().to_string());
199 let config = OroConfigOptions::new().global(false).load()?;
200 env::remove_var("ORO_CONFIG_STORE");
201 assert_eq!(
202 config.get_string("store").into_diagnostic()?,
203 dir.path().display().to_string()
204 );
205 Ok(())
206 }
207
208 #[test]
209 fn global_config() -> Result<()> {
210 let dir = tempdir().into_diagnostic()?;
211 let file = dir.path().join("oro.kdl");
212 fs::write(&file, "options{\nstore \"hello world\"\n}").into_diagnostic()?;
213 let config = OroConfigOptions::new()
214 .env(false)
215 .global_config_file(Some(file))
216 .load()?;
217 assert_eq!(
218 config.get_string("store").into_diagnostic()?,
219 String::from("hello world")
220 );
221 Ok(())
222 }
223
224 #[test]
225 fn missing_config() -> Result<()> {
226 let config = OroConfigOptions::new().global(false).env(false).load()?;
227 assert!(config.get_string("store").is_err());
228 Ok(())
229 }
230}