Skip to main content

miniextendr_api/
named_vector.rs

1//! Named atomic vector wrapper for HashMap/BTreeMap ↔ named R atomic vector conversions.
2//!
3//! By default, `HashMap<String, V>` and `BTreeMap<String, V>` convert to/from named R
4//! lists (VECSXP). This module provides [`NamedVector`] for converting to/from named
5//! **atomic** vectors (INTSXP, REALSXP, LGLSXP, RAWSXP, STRSXP) instead — a more
6//! compact and idiomatic representation when values are scalar.
7//!
8//! # Example
9//!
10//! ```ignore
11//! use std::collections::HashMap;
12//! use miniextendr_api::NamedVector;
13//!
14//! #[miniextendr]
15//! fn make_scores() -> NamedVector<HashMap<String, i32>> {
16//!     let mut m = HashMap::new();
17//!     m.insert("alice".into(), 95);
18//!     m.insert("bob".into(), 87);
19//!     NamedVector(m)
20//! }
21//! // In R: make_scores() returns c(alice = 95L, bob = 87L)
22//! ```
23
24use std::collections::{BTreeMap, HashMap, HashSet};
25
26use crate::ffi::{self, SEXP, SEXPTYPE, SexpExt};
27use crate::from_r::{SexpError, SexpTypeError, TryFromSexp};
28use crate::into_r::IntoR;
29
30// region: AtomicElement trait
31
32/// Marker trait for types that can be elements of named atomic R vectors.
33///
34/// Each implementation knows how to convert `Vec<Self>` to/from an R atomic
35/// vector (INTSXP, REALSXP, LGLSXP, RAWSXP, or STRSXP).
36pub trait AtomicElement: Sized {
37    /// Convert a Rust vector to an R atomic SEXP.
38    fn vec_to_sexp(values: Vec<Self>) -> SEXP;
39
40    /// Convert an R atomic SEXP to a Rust vector.
41    fn vec_from_sexp(sexp: SEXP) -> Result<Vec<Self>, SexpError>;
42}
43
44// --- Primitive numeric types (delegate to existing IntoR / TryFromSexp) ---
45
46impl AtomicElement for i32 {
47    fn vec_to_sexp(values: Vec<Self>) -> SEXP {
48        values.into_sexp()
49    }
50
51    fn vec_from_sexp(sexp: SEXP) -> Result<Vec<Self>, SexpError> {
52        let actual = sexp.type_of();
53        if actual != SEXPTYPE::INTSXP {
54            return Err(SexpTypeError {
55                expected: SEXPTYPE::INTSXP,
56                actual,
57            }
58            .into());
59        }
60        let slice: &[i32] = TryFromSexp::try_from_sexp(sexp)?;
61        Ok(slice.to_vec())
62    }
63}
64
65impl AtomicElement for f64 {
66    fn vec_to_sexp(values: Vec<Self>) -> SEXP {
67        values.into_sexp()
68    }
69
70    fn vec_from_sexp(sexp: SEXP) -> Result<Vec<Self>, SexpError> {
71        let actual = sexp.type_of();
72        if actual != SEXPTYPE::REALSXP {
73            return Err(SexpTypeError {
74                expected: SEXPTYPE::REALSXP,
75                actual,
76            }
77            .into());
78        }
79        let slice: &[f64] = TryFromSexp::try_from_sexp(sexp)?;
80        Ok(slice.to_vec())
81    }
82}
83
84impl AtomicElement for u8 {
85    fn vec_to_sexp(values: Vec<Self>) -> SEXP {
86        values.into_sexp()
87    }
88
89    fn vec_from_sexp(sexp: SEXP) -> Result<Vec<Self>, SexpError> {
90        let actual = sexp.type_of();
91        if actual != SEXPTYPE::RAWSXP {
92            return Err(SexpTypeError {
93                expected: SEXPTYPE::RAWSXP,
94                actual,
95            }
96            .into());
97        }
98        let slice: &[u8] = TryFromSexp::try_from_sexp(sexp)?;
99        Ok(slice.to_vec())
100    }
101}
102
103// --- Bool (non-NA) ---
104
105impl AtomicElement for bool {
106    fn vec_to_sexp(values: Vec<Self>) -> SEXP {
107        values.into_sexp()
108    }
109
110    fn vec_from_sexp(sexp: SEXP) -> Result<Vec<Self>, SexpError> {
111        <Vec<bool>>::try_from_sexp(sexp)
112    }
113}
114
115// --- String (non-NA) ---
116
117impl AtomicElement for String {
118    fn vec_to_sexp(values: Vec<Self>) -> SEXP {
119        values.into_sexp()
120    }
121
122    fn vec_from_sexp(sexp: SEXP) -> Result<Vec<Self>, SexpError> {
123        <Vec<String>>::try_from_sexp(sexp)
124    }
125}
126
127// --- Option<T> types (NA-aware) ---
128
129impl AtomicElement for Option<i32> {
130    fn vec_to_sexp(values: Vec<Self>) -> SEXP {
131        values.into_sexp()
132    }
133
134    fn vec_from_sexp(sexp: SEXP) -> Result<Vec<Self>, SexpError> {
135        <Vec<Option<i32>>>::try_from_sexp(sexp)
136    }
137}
138
139impl AtomicElement for Option<f64> {
140    fn vec_to_sexp(values: Vec<Self>) -> SEXP {
141        values.into_sexp()
142    }
143
144    fn vec_from_sexp(sexp: SEXP) -> Result<Vec<Self>, SexpError> {
145        <Vec<Option<f64>>>::try_from_sexp(sexp)
146    }
147}
148
149impl AtomicElement for Option<bool> {
150    fn vec_to_sexp(values: Vec<Self>) -> SEXP {
151        values.into_sexp()
152    }
153
154    fn vec_from_sexp(sexp: SEXP) -> Result<Vec<Self>, SexpError> {
155        <Vec<Option<bool>>>::try_from_sexp(sexp)
156    }
157}
158
159impl AtomicElement for Option<String> {
160    fn vec_to_sexp(values: Vec<Self>) -> SEXP {
161        values.into_sexp()
162    }
163
164    fn vec_from_sexp(sexp: SEXP) -> Result<Vec<Self>, SexpError> {
165        <Vec<Option<String>>>::try_from_sexp(sexp)
166    }
167}
168// endregion
169
170// region: NamedVector wrapper
171
172/// Wrapper that converts a map to/from a **named atomic R vector** instead of a
173/// named list.
174///
175/// The inner map must have `String` keys and values that implement [`AtomicElement`].
176///
177/// # Supported value types
178///
179/// | Rust type | R SEXPTYPE |
180/// |-----------|-----------|
181/// | `i32` | INTSXP |
182/// | `f64` | REALSXP |
183/// | `u8` | RAWSXP |
184/// | `bool` | LGLSXP |
185/// | `String` | STRSXP |
186/// | `Option<i32>` | INTSXP (NA = NA_INTEGER) |
187/// | `Option<f64>` | REALSXP (NA = NA_REAL) |
188/// | `Option<bool>` | LGLSXP (NA = NA_LOGICAL) |
189/// | `Option<String>` | STRSXP (NA = NA_character_) |
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct NamedVector<M>(pub M);
192
193impl<M> NamedVector<M> {
194    /// Unwrap, returning the inner map.
195    pub fn into_inner(self) -> M {
196        self.0
197    }
198}
199
200impl<M> From<M> for NamedVector<M> {
201    fn from(m: M) -> Self {
202        NamedVector(m)
203    }
204}
205// endregion
206
207// region: Helpers
208
209/// Set names attribute on an R SEXP from a slice of name-like values.
210///
211/// # Safety
212///
213/// `sexp` must be a valid, protected SEXP. Caller must manage protect stack.
214pub(crate) unsafe fn set_names_on_sexp<S: AsRef<str>>(sexp: SEXP, keys: &[S]) {
215    unsafe {
216        let n = keys.len();
217        let names = ffi::Rf_allocVector(SEXPTYPE::STRSXP, n as ffi::R_xlen_t);
218        ffi::Rf_protect(names);
219
220        for (i, key) in keys.iter().enumerate() {
221            let s = key.as_ref();
222            let charsxp = SEXP::charsxp(s);
223            names.set_string_elt(i as ffi::R_xlen_t, charsxp);
224        }
225
226        sexp.set_names(names);
227        ffi::Rf_unprotect(1);
228    }
229}
230
231/// Extract names from an R SEXP with strict validation.
232///
233/// Errors on: missing names attribute, NA names, empty names, duplicate names.
234fn extract_names_strict(sexp: SEXP) -> Result<Vec<String>, SexpError> {
235    use ffi::Rf_translateCharUTF8;
236
237    let names = sexp.get_names();
238    let len = sexp.len();
239
240    if names.type_of() != SEXPTYPE::STRSXP || names.len() != len {
241        return Err(SexpError::InvalidValue(
242            "NamedVector requires a names attribute on the input vector".to_string(),
243        ));
244    }
245
246    let mut result = Vec::with_capacity(len);
247    let mut seen = HashSet::with_capacity(len);
248
249    for i in 0..len {
250        let charsxp = names.string_elt(i as ffi::R_xlen_t);
251
252        // Reject NA names
253        if charsxp == SEXP::na_string() {
254            return Err(SexpError::InvalidValue(
255                "NamedVector does not allow NA names".to_string(),
256            ));
257        }
258
259        let c_str = unsafe { Rf_translateCharUTF8(charsxp) };
260        if c_str.is_null() {
261            return Err(SexpError::InvalidValue(
262                "NamedVector does not allow NA names".to_string(),
263            ));
264        }
265
266        let name = unsafe { std::ffi::CStr::from_ptr(c_str) }
267            .to_str()
268            .unwrap_or("");
269
270        // Reject empty names
271        if name.is_empty() {
272            return Err(SexpError::InvalidValue(
273                "NamedVector does not allow empty names".to_string(),
274            ));
275        }
276
277        // Reject duplicate names
278        if !seen.insert(name.to_string()) {
279            return Err(SexpError::DuplicateName(name.to_string()));
280        }
281
282        result.push(name.to_string());
283    }
284
285    Ok(result)
286}
287// endregion
288
289// region: IntoR impls
290
291impl<V: AtomicElement> IntoR for NamedVector<HashMap<String, V>> {
292    type Error = std::convert::Infallible;
293    fn try_into_sexp(self) -> Result<crate::ffi::SEXP, Self::Error> {
294        Ok(self.into_sexp())
295    }
296    unsafe fn try_into_sexp_unchecked(self) -> Result<crate::ffi::SEXP, Self::Error> {
297        self.try_into_sexp()
298    }
299    fn into_sexp(self) -> SEXP {
300        let (keys, values): (Vec<String>, Vec<V>) = self.0.into_iter().unzip();
301        let sexp = V::vec_to_sexp(values);
302        unsafe {
303            ffi::Rf_protect(sexp);
304            set_names_on_sexp(sexp, &keys);
305            ffi::Rf_unprotect(1);
306        }
307        sexp
308    }
309}
310
311impl<V: AtomicElement> IntoR for NamedVector<BTreeMap<String, V>> {
312    type Error = std::convert::Infallible;
313    fn try_into_sexp(self) -> Result<crate::ffi::SEXP, Self::Error> {
314        Ok(self.into_sexp())
315    }
316    unsafe fn try_into_sexp_unchecked(self) -> Result<crate::ffi::SEXP, Self::Error> {
317        self.try_into_sexp()
318    }
319    fn into_sexp(self) -> SEXP {
320        let (keys, values): (Vec<String>, Vec<V>) = self.0.into_iter().unzip();
321        let sexp = V::vec_to_sexp(values);
322        unsafe {
323            ffi::Rf_protect(sexp);
324            set_names_on_sexp(sexp, &keys);
325            ffi::Rf_unprotect(1);
326        }
327        sexp
328    }
329}
330// endregion
331
332// region: TryFromSexp impls
333
334impl<V: AtomicElement> TryFromSexp for NamedVector<HashMap<String, V>> {
335    type Error = SexpError;
336
337    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
338        let names = extract_names_strict(sexp)?;
339        let values = V::vec_from_sexp(sexp)?;
340
341        let mut map = HashMap::with_capacity(names.len());
342        for (k, v) in names.into_iter().zip(values) {
343            map.insert(k, v);
344        }
345        Ok(NamedVector(map))
346    }
347}
348
349impl<V: AtomicElement> TryFromSexp for NamedVector<BTreeMap<String, V>> {
350    type Error = SexpError;
351
352    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
353        let names = extract_names_strict(sexp)?;
354        let values = V::vec_from_sexp(sexp)?;
355
356        let mut map = BTreeMap::new();
357        for (k, v) in names.into_iter().zip(values) {
358            map.insert(k, v);
359        }
360        Ok(NamedVector(map))
361    }
362}
363// endregion