use std::collections::BTreeMap;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Converter {
None,
Bool,
Int,
IntSi,
List,
}
#[derive(Clone, Debug)]
pub struct Option {
name: String,
default: std::option::Option<String>,
converter: Converter,
}
impl Option {
pub fn string(name: &str, default: std::option::Option<&str>) -> Self {
Option {
name: name.to_string(),
default: default.map(|s| s.to_string()),
converter: Converter::None,
}
}
pub fn with_converter(
name: &str,
default: std::option::Option<&str>,
converter: Converter,
) -> Self {
Option {
name: name.to_string(),
default: default.map(|s| s.to_string()),
converter,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn default(&self) -> std::option::Option<&str> {
self.default.as_deref()
}
pub fn convert_from_unicode(&self, value: &str) -> std::option::Option<String> {
match self.converter {
Converter::None => Some(value.to_string()),
Converter::Bool => {
bool_from_store(value).map(|b| if b { "True" } else { "False" }.to_string())
}
Converter::Int => int_from_store(value).map(|i| i.to_string()),
Converter::IntSi => int_si_from_store(value).map(|i| i.to_string()),
Converter::List => Some(list_from_store(value).join(",")),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct OptionRegistry {
options: BTreeMap<String, Option>,
}
impl OptionRegistry {
pub fn new() -> Self {
OptionRegistry::default()
}
pub fn with_defaults() -> Self {
let mut r = OptionRegistry::new();
r.register(Option::string("stacked_on_location", None));
r.register(Option::string("bound_location", None));
r.register(Option::with_converter("bound", None, Converter::Bool));
r.register(Option::string("parent_location", None));
r.register(Option::string("push_location", None));
r.register(Option::string("public_branch", None));
r.register(Option::string("submit_branch", None));
r.register(Option::string("nickname", None));
r.register(Option::string("default_format", Some("2a")));
r.register(Option::with_converter(
"append_revisions_only",
None,
Converter::Bool,
));
r
}
pub fn register(&mut self, option: Option) {
self.options.insert(option.name.clone(), option);
}
pub fn get(&self, name: &str) -> std::option::Option<&Option> {
self.options.get(name)
}
}
pub fn bool_from_store(s: &str) -> std::option::Option<bool> {
match s.trim().to_ascii_lowercase().as_str() {
"yes" | "y" | "on" | "true" | "1" => Some(true),
"no" | "n" | "off" | "false" | "0" => Some(false),
_ => None,
}
}
pub fn int_from_store(s: &str) -> std::option::Option<i64> {
s.trim().parse().ok()
}
pub fn int_si_from_store(s: &str) -> std::option::Option<i64> {
let s = s.trim();
let (digits, rest) = s
.find(|c: char| !c.is_ascii_digit())
.map(|i| (&s[..i], &s[i..]))
.unwrap_or((s, ""));
if digits.is_empty() {
return None;
}
let base: i64 = digits.parse().ok()?;
let rest = rest.strip_suffix('b').or(Some(rest)).unwrap_or(rest);
let rest = rest.strip_suffix('B').unwrap_or(rest);
let mult = match rest.to_ascii_uppercase().as_str() {
"" => 1,
"K" => 1_000,
"M" => 1_000_000,
"G" => 1_000_000_000,
_ => return None,
};
Some(base * mult)
}
pub fn list_from_store(s: &str) -> Vec<String> {
let s = s.trim();
if s.is_empty() {
return Vec::new();
}
s.split(',')
.map(|item| item.trim().to_string())
.filter(|item| !item.is_empty())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bool_from_store_accepts_breezy_spellings() {
for t in ["yes", "Y", "on", "True", "1"] {
assert_eq!(bool_from_store(t), Some(true), "{t}");
}
for f in ["no", "N", "off", "False", "0"] {
assert_eq!(bool_from_store(f), Some(false), "{f}");
}
assert_eq!(bool_from_store("maybe"), None);
}
#[test]
fn int_si_from_store_scales() {
assert_eq!(int_si_from_store("20"), Some(20));
assert_eq!(int_si_from_store("20K"), Some(20_000));
assert_eq!(int_si_from_store("20MB"), Some(20_000_000));
assert_eq!(int_si_from_store("1G"), Some(1_000_000_000));
assert_eq!(int_si_from_store("xyz"), None);
assert_eq!(int_si_from_store("20T"), None);
}
#[test]
fn list_from_store_splits() {
assert_eq!(list_from_store(""), Vec::<String>::new());
assert_eq!(list_from_store("a"), vec!["a".to_string()]);
assert_eq!(
list_from_store("a, b ,c"),
vec!["a".to_string(), "b".to_string(), "c".to_string()]
);
}
#[test]
fn registry_defaults_present() {
let r = OptionRegistry::with_defaults();
assert_eq!(
r.get("default_format").and_then(|o| o.default()),
Some("2a")
);
assert!(r.get("stacked_on_location").unwrap().default().is_none());
assert_eq!(r.get("bound").unwrap().converter, Converter::Bool);
}
#[test]
fn convert_bool_normalizes() {
let o = Option::with_converter("bound", None, Converter::Bool);
assert_eq!(o.convert_from_unicode("yes").as_deref(), Some("True"));
assert_eq!(o.convert_from_unicode("0").as_deref(), Some("False"));
assert_eq!(o.convert_from_unicode("bogus"), None);
}
#[test]
fn name_and_default_round_trip() {
let o = Option::string("push_location", Some("../trunk"));
assert_eq!(o.name(), "push_location");
assert_eq!(o.default(), Some("../trunk"));
let o = Option::string("nickname", None);
assert_eq!(o.name(), "nickname");
assert_eq!(o.default(), None);
}
#[test]
fn int_from_store_parses_and_rejects() {
assert_eq!(int_from_store("42"), Some(42));
assert_eq!(int_from_store(" -7 "), Some(-7));
assert_eq!(int_from_store("0"), Some(0));
assert_eq!(int_from_store("not a number"), None);
assert_eq!(int_from_store(""), None);
}
#[test]
fn convert_int_uses_int_from_store() {
let o = Option::with_converter("n", None, Converter::Int);
assert_eq!(o.convert_from_unicode("42").as_deref(), Some("42"));
assert_eq!(o.convert_from_unicode("nope"), None);
}
}