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