Skip to main content

kernel/
port_entry.rs

1//! One configured port: which adapter (`type`) and its options.
2//!
3//! `[source]`, `[stream]`, and every `[sinks.<name>]` table in `flusso.toml`
4//! is a [`PortEntry`]. The config layer reads the `type` and carries the rest
5//! as an opaque [`Options`] map; the composition root looks the kind up in
6//! its adapter registry and hands the options to that adapter's config type.
7//! Serialized as the file writes it: `type = "…"` plus the options flattened
8//! beside it, keys sorted, so the lock is byte-stable.
9//!
10//! ```
11//! use kernel::PortEntry;
12//!
13//! let entry: PortEntry = toml::from_str(r#"
14//!     type = "opensearch"
15//!     url = "https://search:9200"
16//!     batch_size = 500
17//! "#).unwrap();
18//! assert_eq!(entry.kind, "opensearch");
19//! assert_eq!(entry.options.get("batch_size").and_then(|v| v.as_i64()), Some(500));
20//! assert_eq!(toml::to_string(&entry).unwrap(), "type = \"opensearch\"\nbatch_size = 500\nurl = \"https://search:9200\"\n");
21//! ```
22
23use serde::{Deserialize, Serialize};
24
25use crate::options::Options;
26
27/// One port entry of a deployment: the adapter kind and its options.
28#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29pub struct PortEntry {
30    /// The adapter that implements the port, as written in `type = "…"`.
31    #[serde(rename = "type")]
32    pub kind: String,
33    /// Everything else under the table, uninterpreted here.
34    #[serde(flatten)]
35    pub options: Options,
36}
37
38impl PortEntry {
39    /// An entry of `kind` with no options, so every adapter default applies.
40    pub fn new(kind: impl Into<String>) -> Self {
41        Self {
42            kind: kind.into(),
43            options: Options::empty(),
44        }
45    }
46
47    /// An entry of `kind` with the given options.
48    pub fn with_options(kind: impl Into<String>, options: Options) -> Self {
49        Self {
50            kind: kind.into(),
51            options,
52        }
53    }
54}
55
56#[cfg(test)]
57#[allow(clippy::unwrap_used)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn type_alone_is_an_empty_options_map() {
63        let entry: PortEntry = toml::from_str("type = \"stdout\"\n").unwrap();
64        assert_eq!(entry, PortEntry::new("stdout"));
65        assert!(entry.options.is_empty());
66    }
67
68    #[test]
69    fn missing_type_is_an_error() {
70        let error = toml::from_str::<PortEntry>("pretty = true\n").unwrap_err();
71        assert!(error.to_string().contains("type"), "{error}");
72    }
73
74    #[test]
75    fn nested_tables_round_trip() {
76        let text = "type = \"postgres\"\n\n[connection_url]\nhost = \"db\"\nport = 5432\n";
77        let entry: PortEntry = toml::from_str(text).unwrap();
78        let again = toml::to_string(&entry).unwrap();
79        assert_eq!(again, text);
80    }
81}
82
83/// The uninterpreted shape: a `type` plus any table. The composition root
84/// replaces this with one alternative per registered adapter when it assembles
85/// the editor schema.
86impl schemars::JsonSchema for PortEntry {
87    fn schema_name() -> std::borrow::Cow<'static, str> {
88        std::borrow::Cow::Borrowed("PortEntry")
89    }
90
91    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
92        schemars::json_schema!({
93            "type": "object",
94            "required": ["type"],
95            "properties": {
96                "type": { "type": "string", "description": "The adapter that implements this port." }
97            },
98            "additionalProperties": true
99        })
100    }
101}