Skip to main content

kernel/
options.rs

1//! The neutral, ordered value tree a port entry carries between the config
2//! layer and the adapter that understands it.
3//!
4//! The config crate reads `[source]`, `[stream]`, and `[sinks.<name>]` as a
5//! `type` plus **whatever else is there**, without interpreting it. That
6//! remainder is an [`Options`] map. The adapter for that `type` turns it into
7//! its own config struct with [`Options::deserialize_into`], where serde's
8//! `deny_unknown_fields` rejects a typo. The kernel therefore names no adapter
9//! and no file format, yet every option still validates strictly, one layer
10//! later.
11//!
12//! [`OptionValue`] is one node of the tree: a scalar, an array, or a map with
13//! sorted keys. Sorted keys are what make a compiled lock byte-stable.
14//!
15//! ```
16//! use kernel::Options;
17//! use serde::Deserialize;
18//!
19//! #[derive(Debug, Deserialize)]
20//! #[serde(deny_unknown_fields)]
21//! struct Channel { capacity: usize }
22//!
23//! let options: Options = toml::from_str(r#"capacity = 512"#).unwrap();
24//! let channel: Channel = options.clone().deserialize_into().unwrap();
25//! assert_eq!(channel.capacity, 512);
26//!
27//! let typo: Options = toml::from_str(r#"capacty = 512"#).unwrap();
28//! let error = typo.deserialize_into::<Channel>().unwrap_err().to_string();
29//! assert!(error.contains("unknown field `capacty`"), "{error}");
30//! ```
31
32use 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/// The options of one port entry: everything under `[source]`, `[stream]`, or
40/// `[sinks.<name>]` except its `type`, keyed in sorted order.
41#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
42#[serde(transparent)]
43pub struct Options(pub BTreeMap<String, OptionValue>);
44
45impl Options {
46    /// An empty options map: the entry's `type` with no settings, so every
47    /// adapter default applies.
48    pub fn empty() -> Self {
49        Self::default()
50    }
51
52    /// Whether no option was given.
53    pub fn is_empty(&self) -> bool {
54        self.0.is_empty()
55    }
56
57    /// Read one top-level option.
58    pub fn get(&self, key: &str) -> Option<&OptionValue> {
59        self.0.get(key)
60    }
61
62    /// Set one top-level option, replacing an existing value. Used by the
63    /// composition root to lay a flag or environment override over the file.
64    pub fn insert(&mut self, key: impl Into<String>, value: impl Into<OptionValue>) {
65        self.0.insert(key.into(), value.into());
66    }
67
68    /// Turn the tree into an adapter's typed config. The adapter's struct
69    /// decides the strictness (`deny_unknown_fields`, defaults, required
70    /// fields); an error names the offending field.
71    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    /// Build the tree from any serializable struct, typically an adapter's
76    /// config: the reverse of [`deserialize_into`](Self::deserialize_into),
77    /// used to render an adapter's example and to write a config back out.
78    /// Fails if the value is not a map at the top level.
79    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/// One node of an options tree.
97#[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    /// The node's kind, for error messages.
110    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    /// The string inside, if this is a string node.
123    pub fn as_str(&self) -> Option<&str> {
124        match self {
125            OptionValue::String(s) => Some(s),
126            _ => None,
127        }
128    }
129
130    /// The boolean inside, if this is a boolean node.
131    pub fn as_bool(&self) -> Option<bool> {
132        match self {
133            OptionValue::Bool(b) => Some(*b),
134            _ => None,
135        }
136    }
137
138    /// The integer inside, if this is an integer node.
139    pub fn as_i64(&self) -> Option<i64> {
140        match self {
141            OptionValue::Integer(i) => Some(*i),
142            _ => None,
143        }
144    }
145
146    /// The map inside, if this is a table node.
147    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/// An options tree could not be turned into an adapter's config (or back): a
330/// missing or unknown field, a wrong type, an out-of-range number. The message
331/// names the field.
332#[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
443/// An options map is "any table" to the schema: the adapter's own schema is
444/// what constrains it, spliced in by the composition root per `type`.
445impl 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}