Skip to main content

miniextendr_api/
match_arg.rs

1//! `match.arg`-style enum conversion for R string arguments.
2//!
3//! This module provides the [`MatchArg`] trait for converting between Rust
4//! fieldless enums and R character strings with `match.arg` semantics
5//! (exact match or unique partial matching).
6//!
7//! # Usage
8//!
9//! ```ignore
10//! use miniextendr_api::MatchArg;
11//!
12//! #[derive(Copy, Clone, MatchArg)]
13//! #[match_arg(rename_all = "snake_case")]
14//! enum Mode {
15//!     Fast,
16//!     Safe,
17//!     Debug,
18//! }
19//!
20//! #[miniextendr]
21//! fn run(#[miniextendr(match_arg)] mode: Mode) -> String {
22//!     format!("{mode:?}")
23//! }
24//! ```
25//!
26//! The generated R wrapper uses `base::match.arg()` for validation before
27//! the main `.Call()`, giving users familiar R error messages and partial
28//! matching.
29
30use crate::ffi::{self, SEXP, SEXPTYPE, SexpExt};
31
32/// Trait for enum types that support `match.arg`-style string conversion.
33///
34/// Implementors provide a fixed set of choice strings and bidirectional
35/// conversion between enum variants and their string representations.
36///
37/// Use `#[derive(MatchArg)]` to auto-generate this implementation.
38pub trait MatchArg: Sized + Copy + 'static {
39    /// The canonical choice strings, in variant declaration order.
40    ///
41    /// The first choice is the default when the R argument is `NULL`.
42    const CHOICES: &'static [&'static str];
43
44    /// Convert a choice string to the corresponding enum variant.
45    ///
46    /// Returns `None` if the string doesn't match any choice exactly.
47    fn from_choice(choice: &str) -> Option<Self>;
48
49    /// Convert the enum variant to its canonical choice string.
50    fn to_choice(self) -> &'static str;
51}
52
53/// Error type for `MatchArg` conversion failures.
54#[derive(Debug, Clone)]
55pub enum MatchArgError {
56    /// The SEXP was not a character or factor type.
57    InvalidType(SEXPTYPE),
58    /// The input had length != 1.
59    InvalidLength(usize),
60    /// The input was NA.
61    IsNa,
62    /// No choice matched the input.
63    NoMatch {
64        /// The input string that didn't match.
65        input: String,
66        /// The valid choices.
67        choices: &'static [&'static str],
68    },
69}
70
71impl std::fmt::Display for MatchArgError {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        match self {
74            MatchArgError::InvalidType(ty) => {
75                write!(f, "match.arg: expected character or factor, got {:?}", ty)
76            }
77            MatchArgError::InvalidLength(len) => {
78                write!(f, "match.arg: expected length 1, got {}", len)
79            }
80            MatchArgError::IsNa => write!(f, "match.arg: input is NA"),
81            MatchArgError::NoMatch { input, choices } => {
82                write!(
83                    f,
84                    "'arg' should be one of {}, got {:?}",
85                    choices
86                        .iter()
87                        .map(|c| format!("{:?}", c))
88                        .collect::<Vec<_>>()
89                        .join(", "),
90                    input,
91                )
92            }
93        }
94    }
95}
96
97impl std::error::Error for MatchArgError {}
98
99impl From<MatchArgError> for crate::from_r::SexpError {
100    fn from(e: MatchArgError) -> Self {
101        crate::from_r::SexpError::InvalidValue(e.to_string())
102    }
103}
104
105/// Build an R character vector (STRSXP) from the choices of a `MatchArg` type.
106///
107/// This is called by generated choices-helper C wrappers to provide the
108/// choice list to `base::match.arg()` in the R wrapper.
109pub fn choices_sexp<T: MatchArg>() -> SEXP {
110    let choices = <T as MatchArg>::CHOICES;
111    unsafe {
112        let n = choices.len();
113        let vec = ffi::Rf_allocVector(SEXPTYPE::STRSXP, n as ffi::R_xlen_t);
114        ffi::Rf_protect(vec);
115        for (i, s) in choices.iter().enumerate() {
116            let charsxp = if s.is_empty() {
117                SEXP::blank_string()
118            } else {
119                SEXP::charsxp(s)
120            };
121            vec.set_string_elt(i as ffi::R_xlen_t, charsxp);
122        }
123        ffi::Rf_unprotect(1);
124        vec
125    }
126}
127
128/// Extract a string from an R SEXP (STRSXP or factor) and match it against
129/// the choices of a `MatchArg` type.
130///
131/// This is used by the generated `TryFromSexp` implementation.
132pub fn match_arg_from_sexp<T: MatchArg>(sexp: SEXP) -> Result<T, MatchArgError> {
133    let actual_type = sexp.type_of();
134
135    // Extract the string value
136    let input = match actual_type {
137        SEXPTYPE::STRSXP => {
138            let len = sexp.len();
139            if len != 1 {
140                return Err(MatchArgError::InvalidLength(len));
141            }
142            let charsxp = sexp.string_elt(0);
143            if charsxp == SEXP::na_string() {
144                return Err(MatchArgError::IsNa);
145            }
146            let c_str = unsafe { ffi::Rf_translateCharUTF8(charsxp) };
147            let rust_str = unsafe { std::ffi::CStr::from_ptr(c_str) };
148            rust_str.to_str().unwrap_or("").to_string()
149        }
150        // Accept factors: extract the level label
151        SEXPTYPE::INTSXP => {
152            // Check if it's a factor (has "levels" attribute)
153            let levels = sexp.get_levels();
154            if levels.is_nil() || levels.type_of() != SEXPTYPE::STRSXP {
155                return Err(MatchArgError::InvalidType(actual_type));
156            }
157            let len = sexp.len();
158            if len != 1 {
159                return Err(MatchArgError::InvalidLength(len));
160            }
161            let idx = unsafe { *ffi::INTEGER(sexp) };
162            if idx == i32::MIN {
163                // NA_integer_
164                return Err(MatchArgError::IsNa);
165            }
166            // R factor indices are 1-based
167            let level_idx = (idx - 1) as ffi::R_xlen_t;
168            if level_idx < 0 || level_idx >= levels.len() as ffi::R_xlen_t {
169                return Err(MatchArgError::NoMatch {
170                    input: format!("<factor index {}>", idx),
171                    choices: <T as MatchArg>::CHOICES,
172                });
173            }
174            let charsxp = levels.string_elt(level_idx);
175            let c_str = unsafe { ffi::Rf_translateCharUTF8(charsxp) };
176            let rust_str = unsafe { std::ffi::CStr::from_ptr(c_str) };
177            rust_str.to_str().unwrap_or("").to_string()
178        }
179        SEXPTYPE::NILSXP => {
180            // NULL → use first choice (match.arg default behavior)
181            return T::from_choice(<T as MatchArg>::CHOICES[0]).ok_or_else(|| {
182                MatchArgError::NoMatch {
183                    input: String::new(),
184                    choices: <T as MatchArg>::CHOICES,
185                }
186            });
187        }
188        _ => return Err(MatchArgError::InvalidType(actual_type)),
189    };
190
191    // Exact match
192    if let Some(val) = T::from_choice(&input) {
193        return Ok(val);
194    }
195
196    // Unique partial match (like R's match.arg)
197    let mut matches: Vec<(usize, &'static str)> = Vec::new();
198    for (i, choice) in <T as MatchArg>::CHOICES.iter().enumerate() {
199        if choice.starts_with(&input) {
200            matches.push((i, choice));
201        }
202    }
203
204    match matches.len() {
205        1 => T::from_choice(matches[0].1).ok_or(MatchArgError::NoMatch {
206            input,
207            choices: <T as MatchArg>::CHOICES,
208        }),
209        0 => Err(MatchArgError::NoMatch {
210            input,
211            choices: <T as MatchArg>::CHOICES,
212        }),
213        _ => {
214            // Ambiguous — report as no match (R's match.arg would say "ambiguous")
215            Err(MatchArgError::NoMatch {
216                input,
217                choices: <T as MatchArg>::CHOICES,
218            })
219        }
220    }
221}