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}