1use std::collections::BTreeMap;
33use std::fmt;
34
35use serde::de::{self, DeserializeOwned, MapAccess, SeqAccess, Visitor};
36use serde::ser::{SerializeMap, SerializeSeq, Serializer};
37use serde::{Deserialize, Deserializer, Serialize};
38
39#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
42#[serde(transparent)]
43pub struct Options(pub BTreeMap<String, OptionValue>);
44
45impl Options {
46 pub fn empty() -> Self {
49 Self::default()
50 }
51
52 pub fn is_empty(&self) -> bool {
54 self.0.is_empty()
55 }
56
57 pub fn get(&self, key: &str) -> Option<&OptionValue> {
59 self.0.get(key)
60 }
61
62 pub fn insert(&mut self, key: impl Into<String>, value: impl Into<OptionValue>) {
65 self.0.insert(key.into(), value.into());
66 }
67
68 pub fn deserialize_into<T: DeserializeOwned>(self) -> Result<T, OptionsError> {
72 serde_json::from_value(OptionValue::Map(self.0).into_json()).map_err(OptionsError)
73 }
74
75 pub fn from_serialize<T: Serialize>(value: &T) -> Result<Self, OptionsError> {
80 match OptionValue::from_json(serde_json::to_value(value).map_err(OptionsError)?) {
81 OptionValue::Map(map) => Ok(Self(map)),
82 other => Err(OptionsError(de::Error::custom(format!(
83 "expected a map of options, got {}",
84 other.kind()
85 )))),
86 }
87 }
88}
89
90impl FromIterator<(String, OptionValue)> for Options {
91 fn from_iter<I: IntoIterator<Item = (String, OptionValue)>>(iter: I) -> Self {
92 Self(iter.into_iter().collect())
93 }
94}
95
96#[derive(Debug, Clone, PartialEq)]
98pub enum OptionValue {
99 Null,
100 Bool(bool),
101 Integer(i64),
102 Float(f64),
103 String(String),
104 Array(Vec<OptionValue>),
105 Map(BTreeMap<String, OptionValue>),
106}
107
108impl OptionValue {
109 pub fn kind(&self) -> &'static str {
111 match self {
112 OptionValue::Null => "null",
113 OptionValue::Bool(_) => "a boolean",
114 OptionValue::Integer(_) => "an integer",
115 OptionValue::Float(_) => "a float",
116 OptionValue::String(_) => "a string",
117 OptionValue::Array(_) => "an array",
118 OptionValue::Map(_) => "a table",
119 }
120 }
121
122 pub fn as_str(&self) -> Option<&str> {
124 match self {
125 OptionValue::String(s) => Some(s),
126 _ => None,
127 }
128 }
129
130 pub fn as_bool(&self) -> Option<bool> {
132 match self {
133 OptionValue::Bool(b) => Some(*b),
134 _ => None,
135 }
136 }
137
138 pub fn as_i64(&self) -> Option<i64> {
140 match self {
141 OptionValue::Integer(i) => Some(*i),
142 _ => None,
143 }
144 }
145
146 pub fn as_map(&self) -> Option<&BTreeMap<String, OptionValue>> {
148 match self {
149 OptionValue::Map(m) => Some(m),
150 _ => None,
151 }
152 }
153
154 fn into_json(self) -> serde_json::Value {
155 match self {
156 OptionValue::Null => serde_json::Value::Null,
157 OptionValue::Bool(b) => serde_json::Value::Bool(b),
158 OptionValue::Integer(i) => serde_json::Value::from(i),
159 OptionValue::Float(f) => serde_json::Value::from(f),
160 OptionValue::String(s) => serde_json::Value::String(s),
161 OptionValue::Array(items) => {
162 serde_json::Value::Array(items.into_iter().map(Self::into_json).collect())
163 }
164 OptionValue::Map(map) => serde_json::Value::Object(
165 map.into_iter().map(|(k, v)| (k, v.into_json())).collect(),
166 ),
167 }
168 }
169
170 fn from_json(value: serde_json::Value) -> Self {
171 match value {
172 serde_json::Value::Null => OptionValue::Null,
173 serde_json::Value::Bool(b) => OptionValue::Bool(b),
174 serde_json::Value::Number(n) => match n.as_i64() {
175 Some(i) => OptionValue::Integer(i),
176 None => OptionValue::Float(n.as_f64().unwrap_or(f64::NAN)),
177 },
178 serde_json::Value::String(s) => OptionValue::String(s),
179 serde_json::Value::Array(items) => {
180 OptionValue::Array(items.into_iter().map(Self::from_json).collect())
181 }
182 serde_json::Value::Object(map) => OptionValue::Map(
183 map.into_iter()
184 .map(|(k, v)| (k, Self::from_json(v)))
185 .collect(),
186 ),
187 }
188 }
189}
190
191impl From<bool> for OptionValue {
192 fn from(value: bool) -> Self {
193 OptionValue::Bool(value)
194 }
195}
196
197impl From<i64> for OptionValue {
198 fn from(value: i64) -> Self {
199 OptionValue::Integer(value)
200 }
201}
202
203impl From<u32> for OptionValue {
204 fn from(value: u32) -> Self {
205 OptionValue::Integer(i64::from(value))
206 }
207}
208
209impl From<usize> for OptionValue {
210 fn from(value: usize) -> Self {
211 OptionValue::Integer(i64::try_from(value).unwrap_or(i64::MAX))
212 }
213}
214
215impl From<&str> for OptionValue {
216 fn from(value: &str) -> Self {
217 OptionValue::String(value.to_owned())
218 }
219}
220
221impl From<String> for OptionValue {
222 fn from(value: String) -> Self {
223 OptionValue::String(value)
224 }
225}
226
227impl From<Options> for OptionValue {
228 fn from(value: Options) -> Self {
229 OptionValue::Map(value.0)
230 }
231}
232
233impl Serialize for OptionValue {
234 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
235 match self {
236 OptionValue::Null => serializer.serialize_unit(),
237 OptionValue::Bool(b) => serializer.serialize_bool(*b),
238 OptionValue::Integer(i) => serializer.serialize_i64(*i),
239 OptionValue::Float(f) => serializer.serialize_f64(*f),
240 OptionValue::String(s) => serializer.serialize_str(s),
241 OptionValue::Array(items) => {
242 let mut seq = serializer.serialize_seq(Some(items.len()))?;
243 for item in items {
244 seq.serialize_element(item)?;
245 }
246 seq.end()
247 }
248 OptionValue::Map(map) => {
249 let mut out = serializer.serialize_map(Some(map.len()))?;
250 for (key, value) in map {
251 out.serialize_entry(key, value)?;
252 }
253 out.end()
254 }
255 }
256 }
257}
258
259struct OptionValueVisitor;
260
261impl<'de> Visitor<'de> for OptionValueVisitor {
262 type Value = OptionValue;
263
264 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265 f.write_str("a config value (string, number, boolean, array, or table)")
266 }
267
268 fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
269 Ok(OptionValue::Bool(v))
270 }
271
272 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
273 Ok(OptionValue::Integer(v))
274 }
275
276 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
277 i64::try_from(v)
278 .map(OptionValue::Integer)
279 .map_err(|_| de::Error::custom(format!("integer {v} is out of range")))
280 }
281
282 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> {
283 Ok(OptionValue::Float(v))
284 }
285
286 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
287 Ok(OptionValue::String(v.to_owned()))
288 }
289
290 fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
291 Ok(OptionValue::String(v))
292 }
293
294 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
295 Ok(OptionValue::Null)
296 }
297
298 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
299 Ok(OptionValue::Null)
300 }
301
302 fn visit_some<D: Deserializer<'de>>(self, deserializer: D) -> Result<Self::Value, D::Error> {
303 deserializer.deserialize_any(self)
304 }
305
306 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
307 let mut items = Vec::new();
308 while let Some(item) = seq.next_element()? {
309 items.push(item);
310 }
311 Ok(OptionValue::Array(items))
312 }
313
314 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
315 let mut out = BTreeMap::new();
316 while let Some((key, value)) = map.next_entry::<String, OptionValue>()? {
317 out.insert(key, value);
318 }
319 Ok(OptionValue::Map(out))
320 }
321}
322
323impl<'de> Deserialize<'de> for OptionValue {
324 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
325 deserializer.deserialize_any(OptionValueVisitor)
326 }
327}
328
329#[derive(Debug)]
333pub struct OptionsError(serde_json::Error);
334
335impl fmt::Display for OptionsError {
336 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337 fmt::Display::fmt(&self.0, f)
338 }
339}
340
341impl std::error::Error for OptionsError {}
342
343#[cfg(test)]
344#[allow(clippy::unwrap_used)]
345mod tests {
346 use super::*;
347
348 #[derive(Debug, PartialEq, Serialize, Deserialize)]
349 #[serde(deny_unknown_fields)]
350 struct Sample {
351 url: String,
352 #[serde(default = "default_batch")]
353 batch: u32,
354 #[serde(default, skip_serializing_if = "Option::is_none")]
355 pipeline: Option<String>,
356 #[serde(default)]
357 tags: Vec<String>,
358 }
359
360 fn default_batch() -> u32 {
361 1000
362 }
363
364 #[test]
365 fn deserializes_with_defaults_and_nested_values() {
366 let options: Options =
367 toml::from_str("url = \"http://x\"\ntags = [\"a\", \"b\"]\n").unwrap();
368 let sample: Sample = options.deserialize_into().unwrap();
369 assert_eq!(
370 sample,
371 Sample {
372 url: "http://x".into(),
373 batch: 1000,
374 pipeline: None,
375 tags: vec!["a".into(), "b".into()],
376 }
377 );
378 }
379
380 #[test]
381 fn unknown_field_is_named() {
382 let options: Options = toml::from_str("url = \"http://x\"\nbatch_sizee = 3\n").unwrap();
383 let error = options
384 .deserialize_into::<Sample>()
385 .unwrap_err()
386 .to_string();
387 assert!(error.contains("unknown field `batch_sizee`"), "{error}");
388 }
389
390 #[test]
391 fn missing_field_is_named() {
392 let options: Options = toml::from_str("batch = 3\n").unwrap();
393 let error = options
394 .deserialize_into::<Sample>()
395 .unwrap_err()
396 .to_string();
397 assert!(error.contains("missing field `url`"), "{error}");
398 }
399
400 #[test]
401 fn round_trips_through_serialize() {
402 let sample = Sample {
403 url: "http://x".into(),
404 batch: 5,
405 pipeline: Some("enrich".into()),
406 tags: vec![],
407 };
408 let options = Options::from_serialize(&sample).unwrap();
409 assert_eq!(options.get("batch"), Some(&OptionValue::Integer(5)));
410 assert_eq!(
411 options.get("pipeline").and_then(OptionValue::as_str),
412 Some("enrich")
413 );
414 let back: Sample = options.deserialize_into().unwrap();
415 assert_eq!(back, sample);
416 }
417
418 #[test]
419 fn keys_serialize_sorted() {
420 let mut options = Options::empty();
421 options.insert("zeta", 1i64);
422 options.insert("alpha", "a");
423 options.insert("mid", true);
424 let toml = toml::to_string(&options).unwrap();
425 assert_eq!(toml, "alpha = \"a\"\nmid = true\nzeta = 1\n");
426 }
427
428 #[test]
429 fn nested_tables_and_null_survive() {
430 let options: Options = toml::from_str("[inner]\nkey = \"v\"\n").unwrap();
431 let inner = options.get("inner").and_then(OptionValue::as_map).unwrap();
432 assert_eq!(inner.get("key").and_then(OptionValue::as_str), Some("v"));
433 assert_eq!(OptionValue::Null.kind(), "null");
434 }
435
436 #[test]
437 fn from_serialize_rejects_non_maps() {
438 let error = Options::from_serialize(&7).unwrap_err().to_string();
439 assert!(error.contains("expected a map of options"), "{error}");
440 }
441}
442
443impl schemars::JsonSchema for Options {
446 fn schema_name() -> std::borrow::Cow<'static, str> {
447 std::borrow::Cow::Borrowed("Options")
448 }
449
450 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
451 schemars::json_schema!({ "type": "object" })
452 }
453}
454
455impl schemars::JsonSchema for OptionValue {
456 fn schema_name() -> std::borrow::Cow<'static, str> {
457 std::borrow::Cow::Borrowed("OptionValue")
458 }
459
460 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
461 schemars::json_schema!(true)
462 }
463}