1use std::{borrow::Cow, collections::HashMap, env, fmt};
2
3pub type ConfigType = &'static str;
4
5#[derive(Debug, Clone)]
6pub enum ValidValue<'a> {
7 Number(i64),
8 String(Cow<'a, str>),
9 Boolean(bool),
10}
11
12pub struct Whsp<'a> {
13 pub config_set: HashMap<&'a str, ConfigOptionBase<'a>>,
14 pub short_options: HashMap<&'a str, &'a str>,
15 pub options: WhspOptions,
16}
17
18#[derive(Debug)]
19pub struct WhspOptions {
20 pub allow_positionals: bool,
21 pub env_prefix: Option<&'static str>,
22 pub usage: Option<&'static str>,
23}
24
25#[derive(Debug)]
26pub struct ConfigOptionBase<'a> {
27 pub config_type: ConfigType,
28 pub short: Option<&'a str>,
29 pub default: Option<ValidValue<'a>>,
30 pub description: Option<&'a str>,
31 pub validate: Option<Validator>,
32 pub multiple: bool,
33}
34
35#[derive(Debug)]
36pub enum Validator {
37 NumberRange(i64, i64),
38 Regex(&'static str),
39 None,
40}
41
42impl<'a> Whsp<'a> {
43 pub fn num(&mut self, fields: HashMap<&'a str, ConfigOptionBase<'a>>) {
44 for (name, mut option) in fields {
45 option.config_type = "number";
46 self.config_set.insert(name, option);
47 }
48 }
49
50 pub fn num_list(&mut self, fields: HashMap<&'a str, ConfigOptionBase<'a>>) {
51 for (name, mut option) in fields {
52 option.config_type = "number";
53 option.multiple = true;
54 self.config_set.insert(name, option);
55 }
56 }
57
58 pub fn opt(&mut self, fields: HashMap<&'a str, ConfigOptionBase<'a>>) {
59 for (name, mut option) in fields {
60 option.config_type = "string";
61 self.config_set.insert(name, option);
62 }
63 }
64
65 pub fn opt_list(&mut self, fields: HashMap<&'a str, ConfigOptionBase<'a>>) {
66 for (name, mut option) in fields {
67 option.config_type = "string";
68 option.multiple = true;
69 self.config_set.insert(name, option);
70 }
71 }
72
73 pub fn flag(&mut self, fields: HashMap<&'a str, ConfigOptionBase<'a>>) {
74 for (name, mut option) in fields {
75 option.config_type = "boolean";
76 self.config_set.insert(name, option);
77 }
78 }
79
80 pub fn flag_list(&mut self, fields: HashMap<&'a str, ConfigOptionBase<'a>>) {
81 for (name, mut option) in fields {
82 option.config_type = "boolean";
83 option.multiple = true;
84 self.config_set.insert(name, option);
85 }
86 }
87
88 pub fn validate_name(
89 &mut self,
90 name: &'a str,
91 option: &ConfigOptionBase<'a>,
92 ) -> Result<(), String> {
93 if !name.chars().all(char::is_alphanumeric) {
94 return Err(format!(
95 "Invalid option name: {name}, must be alphanumeric."
96 ));
97 }
98 if let Some(short) = option.short {
99 if self.short_options.contains_key(short) {
100 return Err(format!("Short option {short} is already in use."));
101 }
102 self.short_options.insert(short, name);
103 }
104 Ok(())
105 }
106
107 pub fn write_env(&self, parsed: &OptionsResult) {
108 if let Some(prefix) = self.options.env_prefix {
109 for (field, value) in &parsed.values {
110 let env_key = to_env_key(prefix, field);
111 let env_value = to_env_val(value);
112 env::set_var(env_key, env_value);
113 }
114 }
115 }
116
117 pub fn parse_raw(&self, args: &'a [String]) -> OptionsResult<'a> {
118 let mut values = HashMap::new();
119 let mut positionals = Vec::new();
120 let mut i = 0;
121
122 while i < args.len() {
123 let arg = &args[i];
124 if let Some(key) = arg.strip_prefix("--") {
125 if let Some(config) = self.config_set.get(key) {
126 if config.config_type == "boolean" {
127 values.insert(key, ValidValue::Boolean(true));
128 } else if i + 1 < args.len() {
129 let val = &args[i + 1];
130 values.insert(
131 key,
132 match config.config_type {
133 "string" => ValidValue::String(val.into()),
134 "number" => ValidValue::Number(val.parse().unwrap()),
135 _ => panic!("Unknown config type"),
136 },
137 );
138 i += 1;
139 }
140 }
141 } else if let Some(short) = arg.strip_prefix('-') {
142 if let Some(&key) = self.short_options.get(short) {
143 if let Some(config) = self.config_set.get(key) {
144 if config.config_type == "boolean" {
145 values.insert(key, ValidValue::Boolean(true));
146 } else if i + 1 < args.len() {
147 let val = &args[i + 1];
148 values.insert(
149 key,
150 match config.config_type {
151 "string" => ValidValue::String(val.into()),
152 "number" => ValidValue::Number(val.parse().unwrap()),
153 _ => panic!("Unknown config type"),
154 },
155 );
156 i += 1;
157 }
158 }
159 }
160 } else {
161 positionals.push(arg.as_str());
162 }
163 i += 1;
164 }
165
166 OptionsResult {
167 values,
168 positionals,
169 }
170 }
171
172 pub fn validate(&self, o: &HashMap<String, ValidValue>) -> Result<(), String> {
173 for (field, value) in o {
174 let config = self
175 .config_set
176 .get(field.as_str())
177 .ok_or(format!("Unknown config option: {field}"))?;
178 validate_options(config, value)?;
179 }
180 Ok(())
181 }
182
183 pub fn set_defaults_from_env(&mut self) {
184 if let Some(prefix) = self.options.env_prefix {
185 for (key, option) in self.config_set.iter_mut() {
186 let env_key = to_env_key(prefix, key);
187 if let Ok(val) = env::var(&env_key) {
188 let valid_val = from_env_val(val, option.config_type);
189 option.default = Some(valid_val);
190 }
191 }
192 }
193 }
194}
195
196impl fmt::Display for ValidValue<'_> {
197 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
198 match self {
199 ValidValue::Number(val) => write!(f, "{val}"),
200 ValidValue::String(val) => write!(f, "{val}"),
201 ValidValue::Boolean(val) => write!(f, "{val}"),
202 }
203 }
204}
205
206impl<'a> ConfigOptionBase<'a> {
207 pub const fn new(
208 config_type: ConfigType,
209 multiple: bool,
210 short: Option<&'a str>,
211 description: Option<&'a str>,
212 ) -> Self {
213 Self {
214 config_type,
215 short,
216 default: None,
217 description,
218 validate: None,
219 multiple,
220 }
221 }
222
223 pub fn validate_value(&self, value: &ValidValue) -> bool {
224 if let Some(ref validate) = self.validate {
225 match *validate {
226 Validator::Regex(regex) => matches!(value, ValidValue::String(s) if regex == s),
227 Validator::NumberRange(min, max) => {
228 matches!(value, ValidValue::Number(num) if *num >= min && *num <= max)
229 },
230 Validator::None => true,
231 }
232 } else {
233 matches!(
234 (self.config_type, value),
235 ("string", ValidValue::String(_))
236 | ("number", ValidValue::Number(_))
237 | ("boolean", ValidValue::Boolean(_))
238 )
239 }
240 }
241}
242
243pub fn to_env_key(prefix: &str, key: &str) -> String {
244 format!("{}_{}", prefix.to_uppercase(), key.to_uppercase())
245}
246
247pub fn from_env_val<'a, E: Into<Cow<'a, str>>>(env: E, config_type: &str) -> ValidValue<'a> {
248 match config_type {
249 "string" => ValidValue::String(env.into()),
250 "number" => ValidValue::Number(env.into().parse().unwrap()),
251 "boolean" => ValidValue::Boolean(env.into() == "1"),
252 _ => panic!("Unknown config type"),
253 }
254}
255
256pub fn to_env_val(value: &ValidValue) -> String {
257 match value {
258 ValidValue::String(v) => v.to_string(),
259 ValidValue::Number(v) => v.to_string(),
260 ValidValue::Boolean(v) => {
261 if *v {
262 "1"
263 } else {
264 "0"
265 }
266 }
267 .to_string(),
268 }
269}
270
271pub fn validate_options(config: &ConfigOptionBase, value: &ValidValue) -> Result<(), String> {
272 if !config.validate_value(value) {
273 return Err(format!("Invalid value {value:?} for option"));
274 }
275 Ok(())
276}
277
278#[derive(Debug)]
279pub struct OptionsResult<'a> {
280 pub values: HashMap<&'a str, ValidValue<'a>>,
281 pub positionals: Vec<&'a str>,
282}