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 crate::from_r::charsxp_to_str;
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 name = unsafe { charsxp_to_str(charsxp) };
260
261        // Reject empty names
262        if name.is_empty() {
263            return Err(SexpError::InvalidValue(
264                "NamedVector does not allow empty names".to_string(),
265            ));
266        }
267
268        // Reject duplicate names
269        if !seen.insert(name.to_string()) {
270            return Err(SexpError::DuplicateName(name.to_string()));
271        }
272
273        result.push(name.to_string());
274    }
275
276    Ok(result)
277}
278// endregion
279
280// region: IntoR impls
281
282impl<V: AtomicElement> IntoR for NamedVector<HashMap<String, V>> {
283    type Error = std::convert::Infallible;
284    fn try_into_sexp(self) -> Result<crate::ffi::SEXP, Self::Error> {
285        Ok(self.into_sexp())
286    }
287    unsafe fn try_into_sexp_unchecked(self) -> Result<crate::ffi::SEXP, Self::Error> {
288        self.try_into_sexp()
289    }
290    fn into_sexp(self) -> SEXP {
291        let (keys, values): (Vec<String>, Vec<V>) = self.0.into_iter().unzip();
292        let sexp = V::vec_to_sexp(values);
293        unsafe {
294            ffi::Rf_protect(sexp);
295            set_names_on_sexp(sexp, &keys);
296            ffi::Rf_unprotect(1);
297        }
298        sexp
299    }
300}
301
302impl<V: AtomicElement> IntoR for NamedVector<BTreeMap<String, V>> {
303    type Error = std::convert::Infallible;
304    fn try_into_sexp(self) -> Result<crate::ffi::SEXP, Self::Error> {
305        Ok(self.into_sexp())
306    }
307    unsafe fn try_into_sexp_unchecked(self) -> Result<crate::ffi::SEXP, Self::Error> {
308        self.try_into_sexp()
309    }
310    fn into_sexp(self) -> SEXP {
311        let (keys, values): (Vec<String>, Vec<V>) = self.0.into_iter().unzip();
312        let sexp = V::vec_to_sexp(values);
313        unsafe {
314            ffi::Rf_protect(sexp);
315            set_names_on_sexp(sexp, &keys);
316            ffi::Rf_unprotect(1);
317        }
318        sexp
319    }
320}
321// endregion
322
323// region: TryFromSexp impls
324
325impl<V: AtomicElement> TryFromSexp for NamedVector<HashMap<String, V>> {
326    type Error = SexpError;
327
328    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
329        let names = extract_names_strict(sexp)?;
330        let values = V::vec_from_sexp(sexp)?;
331
332        let mut map = HashMap::with_capacity(names.len());
333        for (k, v) in names.into_iter().zip(values) {
334            map.insert(k, v);
335        }
336        Ok(NamedVector(map))
337    }
338}
339
340impl<V: AtomicElement> TryFromSexp for NamedVector<BTreeMap<String, V>> {
341    type Error = SexpError;
342
343    fn try_from_sexp(sexp: SEXP) -> Result<Self, Self::Error> {
344        let names = extract_names_strict(sexp)?;
345        let values = V::vec_from_sexp(sexp)?;
346
347        let mut map = BTreeMap::new();
348        for (k, v) in names.into_iter().zip(values) {
349            map.insert(k, v);
350        }
351        Ok(NamedVector(map))
352    }
353}
354// endregion