Skip to main content

confium_api/
options.rs

1//! Option map types passed from Confium into a plugin's `create` entry
2//! point.
3//!
4//! The wire shape mirrors `confium_core::options::Options` exactly so a
5//! pointer of one can be reinterpreted as the other across the FFI
6//! boundary. Plugin authors get typed accessors
7//! ([`OptionView::get_str`], [`OptionView::get_u32`],
8//! [`OptionView::get_map`]) without depending on `confium-core`.
9//!
10//! The proc-macro generated `cfmp_<iface>_create` reinterprets the
11//! `*const Options` argument as `&OptionMap` (they share a layout) and
12//! hands the borrow to the trait method.
13
14use std::collections::HashMap;
15use std::ffi::c_void;
16
17/// Discriminated union of the option values that may flow across the
18/// FFI option map. Layout-compatible with
19/// `confium_core::options::OptionValue`.
20#[derive(Debug, Clone, PartialEq)]
21pub enum OptionValue {
22    String(String),
23    U32(u32),
24    /// A nested option map (used for subkeys like `keyfmt = { encoding = "pem" }`).
25    Map(Box<OptionMap>),
26}
27
28/// A string-keyed map of [`OptionValue`]s, layout-compatible with
29/// `confium_core::options::Options` (`HashMap<String, OptionValue>`).
30pub type OptionMap = HashMap<String, OptionValue>;
31
32/// Read-only view over an [`OptionMap`] borrowed across the FFI.
33///
34/// The plugin contract hands the loader-owned `&OptionMap` to the plugin
35/// for the duration of `create`; this wrapper gives plugin authors a
36/// typed accessor surface without forcing them to construct a
37/// `HashMap<String, String>` themselves.
38pub struct OptionView<'a> {
39    inner: &'a OptionMap,
40}
41
42impl<'a> OptionView<'a> {
43    /// Wrap a borrow over an option map. The macro-generated entry point
44    /// typically reinterprets the incoming `*const Options` as
45    /// `&OptionMap` and hands it here.
46    pub fn new(inner: &'a OptionMap) -> Self {
47        Self { inner }
48    }
49
50    /// Wrap a raw pointer that the loader produced. The pointer must
51    /// point at a value layout-compatible with [`OptionMap`] and remain
52    /// valid for the borrow. A NULL pointer yields `None`.
53    ///
54    /// # Safety
55    ///
56    /// `ptr` must either be NULL or point at a value that is
57    /// layout-compatible with `OptionMap` and valid for `'a`.
58    pub unsafe fn from_raw_ptr(ptr: *const c_void) -> Option<Self> {
59        if ptr.is_null() {
60            return None;
61        }
62        // SAFETY: caller guarantees the pointer is a valid, properly
63        // aligned `&OptionMap` borrow for `'a`.
64        let inner: &'a OptionMap = unsafe { &*(ptr as *const OptionMap) };
65        Some(Self { inner })
66    }
67
68    /// Read a string option. Returns `None` if the key is absent or the
69    /// value isn't a [`OptionValue::String`].
70    pub fn get_str(&self, key: &str) -> Option<&str> {
71        match self.inner.get(key)? {
72            OptionValue::String(s) => Some(s.as_str()),
73            _ => None,
74        }
75    }
76
77    /// Read a u32 option. Returns `None` if the key is absent or the
78    /// value isn't a [`OptionValue::U32`].
79    pub fn get_u32(&self, key: &str) -> Option<u32> {
80        match self.inner.get(key)? {
81            OptionValue::U32(n) => Some(*n),
82            _ => None,
83        }
84    }
85
86    /// Read a nested map option. Returns `None` if the key is absent or
87    /// the value isn't a [`OptionValue::Map`].
88    pub fn get_map(&self, key: &str) -> Option<OptionView<'_>> {
89        match self.inner.get(key)? {
90            OptionValue::Map(m) => Some(OptionView::new(m.as_ref())),
91            _ => None,
92        }
93    }
94
95    /// True if the map contains no entries.
96    pub fn is_empty(&self) -> bool {
97        self.inner.is_empty()
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    fn sample() -> OptionMap {
106        let mut m = OptionMap::new();
107        m.insert(
108            "algorithm".to_string(),
109            OptionValue::String("sha-256".to_string()),
110        );
111        m.insert("output_size".to_string(), OptionValue::U32(32));
112        let mut nested = OptionMap::new();
113        nested.insert(
114            "encoding".to_string(),
115            OptionValue::String("pem".to_string()),
116        );
117        m.insert("keyfmt".to_string(), OptionValue::Map(Box::new(nested)));
118        m
119    }
120
121    #[test]
122    fn reads_string_value() {
123        let m = sample();
124        let view = OptionView::new(&m);
125        assert_eq!(view.get_str("algorithm"), Some("sha-256"));
126    }
127
128    #[test]
129    fn reads_u32_value() {
130        let m = sample();
131        let view = OptionView::new(&m);
132        assert_eq!(view.get_u32("output_size"), Some(32));
133    }
134
135    #[test]
136    fn reads_nested_map() {
137        let m = sample();
138        let view = OptionView::new(&m);
139        let nested = view.get_map("keyfmt").expect("nested map present");
140        assert_eq!(nested.get_str("encoding"), Some("pem"));
141    }
142
143    #[test]
144    fn missing_key_returns_none() {
145        let m = sample();
146        let view = OptionView::new(&m);
147        assert_eq!(view.get_str("missing"), None);
148        assert_eq!(view.get_u32("missing"), None);
149        assert!(view.get_map("missing").is_none());
150    }
151
152    #[test]
153    fn wrong_type_returns_none() {
154        let m = sample();
155        let view = OptionView::new(&m);
156        // algorithm is a string — asking for it as u32/map is a type mismatch.
157        assert_eq!(view.get_u32("algorithm"), None);
158        assert!(view.get_map("algorithm").is_none());
159    }
160
161    #[test]
162    fn null_pointer_yields_none() {
163        let view = unsafe { OptionView::from_raw_ptr(std::ptr::null()) };
164        assert!(view.is_none());
165    }
166}