1use alloc::collections::{BTreeMap, BTreeSet};
4use alloc::string::String;
5use alloc::vec::Vec;
6use core::fmt;
7
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
12#[serde(rename_all = "kebab-case")]
13pub enum ConfigValue {
14 Bool(bool),
15 I64(i64),
16 U64(u64),
17 Text(String),
18 Bytes(Vec<u8>),
19}
20
21#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "kebab-case")]
23pub enum ConfigType {
24 Bool,
25 I64 { minimum: i64, maximum: i64 },
26 U64 { minimum: u64, maximum: u64 },
27 Text { max_bytes: u32 },
28 Choice { values: Vec<String> },
29 Bytes { max_bytes: u32 },
30}
31
32#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
33pub struct ConfigField {
34 pub name: String,
35 pub value_type: ConfigType,
36 pub required: bool,
37 pub default: Option<ConfigValue>,
38}
39
40#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
41pub struct ConfigSchema {
42 pub fields: Vec<ConfigField>,
43}
44
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub enum ConfigError {
47 DuplicateField(String),
48 InvalidField(String),
49 UnknownField(String),
50 MissingField(String),
51 InvalidValue(String),
52}
53
54impl fmt::Display for ConfigError {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(f, "{self:?}")
57 }
58}
59
60#[cfg(feature = "std")]
61impl std::error::Error for ConfigError {}
62
63impl ConfigSchema {
64 pub fn normalize(&mut self) -> Result<(), ConfigError> {
66 self.fields
67 .sort_by(|left, right| left.name.cmp(&right.name));
68 if self
69 .fields
70 .windows(2)
71 .any(|pair| pair[0].name == pair[1].name)
72 {
73 return Err(ConfigError::DuplicateField(
74 self.fields
75 .windows(2)
76 .find(|pair| pair[0].name == pair[1].name)
77 .expect("duplicate was detected")[0]
78 .name
79 .clone(),
80 ));
81 }
82 for field in &mut self.fields {
83 if field.name.is_empty() || (field.required && field.default.is_some()) {
84 return Err(ConfigError::InvalidField(field.name.clone()));
85 }
86 if let ConfigType::Choice { values } = &mut field.value_type {
87 values.sort_unstable();
88 values.dedup();
89 if values.is_empty() || values.iter().any(String::is_empty) {
90 return Err(ConfigError::InvalidField(field.name.clone()));
91 }
92 }
93 if !valid_type(&field.value_type)
94 || field
95 .default
96 .as_ref()
97 .is_some_and(|value| !valid_value(&field.value_type, value))
98 {
99 return Err(ConfigError::InvalidField(field.name.clone()));
100 }
101 }
102 Ok(())
103 }
104
105 pub fn canonicalize(
107 &self,
108 supplied: &BTreeMap<String, ConfigValue>,
109 ) -> Result<BTreeMap<String, ConfigValue>, ConfigError> {
110 let fields: BTreeMap<_, _> = self
111 .fields
112 .iter()
113 .map(|field| (field.name.as_str(), field))
114 .collect();
115 if let Some(unknown) = supplied
116 .keys()
117 .find(|name| !fields.contains_key(name.as_str()))
118 {
119 return Err(ConfigError::UnknownField(unknown.clone()));
120 }
121 let mut normalized = BTreeMap::new();
122 for field in &self.fields {
123 match supplied.get(&field.name).or(field.default.as_ref()) {
124 Some(value) if valid_value(&field.value_type, value) => {
125 normalized.insert(field.name.clone(), value.clone());
126 }
127 Some(_) => return Err(ConfigError::InvalidValue(field.name.clone())),
128 None if field.required => {
129 return Err(ConfigError::MissingField(field.name.clone()));
130 }
131 None => {}
132 }
133 }
134 Ok(normalized)
135 }
136}
137
138fn valid_type(value_type: &ConfigType) -> bool {
139 match value_type {
140 ConfigType::I64 { minimum, maximum } => minimum <= maximum,
141 ConfigType::U64 { minimum, maximum } => minimum <= maximum,
142 ConfigType::Text { max_bytes } | ConfigType::Bytes { max_bytes } => *max_bytes > 0,
143 ConfigType::Choice { values } => {
144 !values.is_empty()
145 && values.iter().all(|value| !value.is_empty())
146 && values.iter().collect::<BTreeSet<_>>().len() == values.len()
147 }
148 ConfigType::Bool => true,
149 }
150}
151
152fn valid_value(value_type: &ConfigType, value: &ConfigValue) -> bool {
153 match (value_type, value) {
154 (ConfigType::Bool, ConfigValue::Bool(_)) => true,
155 (ConfigType::I64 { minimum, maximum }, ConfigValue::I64(value)) => {
156 value >= minimum && value <= maximum
157 }
158 (ConfigType::U64 { minimum, maximum }, ConfigValue::U64(value)) => {
159 value >= minimum && value <= maximum
160 }
161 (ConfigType::Text { max_bytes }, ConfigValue::Text(value)) => {
162 value.len() <= *max_bytes as usize
163 }
164 (ConfigType::Choice { values }, ConfigValue::Text(value)) => values.contains(value),
165 (ConfigType::Bytes { max_bytes }, ConfigValue::Bytes(value)) => {
166 value.len() <= *max_bytes as usize
167 }
168 _ => false,
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use alloc::vec;
175
176 use super::*;
177
178 #[test]
179 fn schema_canonicalizes_order_choices_and_defaults() {
180 let mut schema = ConfigSchema {
181 fields: vec![
182 ConfigField {
183 name: "mode".into(),
184 value_type: ConfigType::Choice {
185 values: vec!["safe".into(), "fast".into(), "safe".into()],
186 },
187 required: false,
188 default: Some(ConfigValue::Text("safe".into())),
189 },
190 ConfigField {
191 name: "channels".into(),
192 value_type: ConfigType::U64 {
193 minimum: 1,
194 maximum: 256,
195 },
196 required: true,
197 default: None,
198 },
199 ],
200 };
201 schema.normalize().unwrap();
202 assert_eq!(schema.fields[0].name, "channels");
203 assert_eq!(
204 schema.canonicalize(&BTreeMap::from([("channels".into(), ConfigValue::U64(64))])),
205 Ok(BTreeMap::from([
206 ("channels".into(), ConfigValue::U64(64)),
207 ("mode".into(), ConfigValue::Text("safe".into())),
208 ]))
209 );
210 }
211
212 #[test]
213 fn instance_rejects_unknown_missing_and_out_of_range_values() {
214 let schema = ConfigSchema {
215 fields: vec![ConfigField {
216 name: "channels".into(),
217 value_type: ConfigType::U64 {
218 minimum: 1,
219 maximum: 256,
220 },
221 required: true,
222 default: None,
223 }],
224 };
225 assert!(matches!(
226 schema.canonicalize(&BTreeMap::new()),
227 Err(ConfigError::MissingField(_))
228 ));
229 assert!(matches!(
230 schema.canonicalize(&BTreeMap::from([(
231 "channels".into(),
232 ConfigValue::U64(257)
233 )])),
234 Err(ConfigError::InvalidValue(_))
235 ));
236 assert!(matches!(
237 schema.canonicalize(&BTreeMap::from([(
238 "surprise".into(),
239 ConfigValue::Bool(true)
240 )])),
241 Err(ConfigError::UnknownField(_))
242 ));
243 }
244}