metarepo_core/
config_setting.rs1use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub enum ConfigValueType {
17 String,
19 Bool,
21 Integer,
23 StringList,
26}
27
28impl ConfigValueType {
29 pub fn label(self) -> &'static str {
31 match self {
32 ConfigValueType::String => "string",
33 ConfigValueType::Bool => "bool",
34 ConfigValueType::Integer => "int",
35 ConfigValueType::StringList => "list",
36 }
37 }
38
39 pub fn from_label(label: &str) -> Option<Self> {
41 match label {
42 "string" => Some(ConfigValueType::String),
43 "bool" => Some(ConfigValueType::Bool),
44 "int" => Some(ConfigValueType::Integer),
45 "list" => Some(ConfigValueType::StringList),
46 _ => None,
47 }
48 }
49
50 pub fn parse(self, raw: &str) -> Result<Value, String> {
54 match self {
55 ConfigValueType::String => Ok(Value::String(raw.to_string())),
56 ConfigValueType::Bool => match raw.trim().to_ascii_lowercase().as_str() {
57 "true" | "1" | "yes" | "on" => Ok(Value::Bool(true)),
58 "false" | "0" | "no" | "off" => Ok(Value::Bool(false)),
59 _ => Err(format!("expected a boolean (true/false), got '{}'", raw)),
60 },
61 ConfigValueType::Integer => raw
62 .trim()
63 .parse::<i64>()
64 .map(|n| Value::Number(n.into()))
65 .map_err(|_| format!("expected an integer, got '{}'", raw)),
66 ConfigValueType::StringList => {
67 let trimmed = raw.trim();
68 if trimmed.starts_with('[') {
69 serde_json::from_str::<Vec<String>>(trimmed)
70 .map(|v| Value::Array(v.into_iter().map(Value::String).collect()))
71 .map_err(|_| format!("expected a JSON array of strings, got '{}'", raw))
72 } else if trimmed.is_empty() {
73 Ok(Value::Array(Vec::new()))
74 } else {
75 Ok(Value::Array(
76 trimmed
77 .split(',')
78 .map(|s| Value::String(s.trim().to_string()))
79 .collect(),
80 ))
81 }
82 }
83 }
84 }
85
86 pub fn matches(self, value: &Value) -> bool {
88 match self {
89 ConfigValueType::String => value.is_string(),
90 ConfigValueType::Bool => value.is_boolean(),
91 ConfigValueType::Integer => value.is_i64() || value.is_u64(),
92 ConfigValueType::StringList => value
93 .as_array()
94 .map(|a| a.iter().all(Value::is_string))
95 .unwrap_or(false),
96 }
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ConfigSetting {
103 pub key: String,
106 pub description: String,
108 pub default: Option<String>,
110 pub value_type: ConfigValueType,
112}
113
114impl ConfigSetting {
115 pub fn new(
117 key: impl Into<String>,
118 description: impl Into<String>,
119 value_type: ConfigValueType,
120 ) -> Self {
121 Self {
122 key: key.into(),
123 description: description.into(),
124 default: None,
125 value_type,
126 }
127 }
128
129 pub fn with_default(mut self, default: impl Into<String>) -> Self {
131 self.default = Some(default.into());
132 self
133 }
134
135 pub fn namespace(&self) -> &str {
137 self.key.split('.').next().unwrap_or(&self.key)
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use serde_json::json;
145
146 #[test]
147 fn bool_parses_common_spellings() {
148 assert_eq!(ConfigValueType::Bool.parse("true"), Ok(json!(true)));
149 assert_eq!(ConfigValueType::Bool.parse("Off"), Ok(json!(false)));
150 assert_eq!(ConfigValueType::Bool.parse("yes"), Ok(json!(true)));
151 assert!(ConfigValueType::Bool.parse("maybe").is_err());
152 }
153
154 #[test]
155 fn integer_parses_and_rejects() {
156 assert_eq!(ConfigValueType::Integer.parse(" 42 "), Ok(json!(42)));
157 assert!(ConfigValueType::Integer.parse("3.5").is_err());
158 }
159
160 #[test]
161 fn list_accepts_comma_and_json() {
162 assert_eq!(
163 ConfigValueType::StringList.parse("a, b ,c"),
164 Ok(json!(["a", "b", "c"]))
165 );
166 assert_eq!(
167 ConfigValueType::StringList.parse(r#"["x","y"]"#),
168 Ok(json!(["x", "y"]))
169 );
170 assert_eq!(ConfigValueType::StringList.parse(""), Ok(json!([])));
171 }
172
173 #[test]
174 fn matches_checks_existing_type() {
175 assert!(ConfigValueType::String.matches(&json!("s")));
176 assert!(!ConfigValueType::String.matches(&json!(1)));
177 assert!(ConfigValueType::StringList.matches(&json!(["a"])));
178 assert!(!ConfigValueType::StringList.matches(&json!([1])));
179 }
180
181 #[test]
182 fn namespace_is_first_segment() {
183 let s = ConfigSetting::new("skill.dest", "d", ConfigValueType::String);
184 assert_eq!(s.namespace(), "skill");
185 }
186}