miniextendr_api/
match_arg.rs1use crate::ffi::{self, SEXP, SEXPTYPE, SexpExt};
31use crate::from_r::{SexpError, TryFromSexp, charsxp_to_str};
32use crate::into_r::IntoR;
33
34pub trait MatchArg: Sized + Copy + 'static {
41 const CHOICES: &'static [&'static str];
45
46 fn from_choice(choice: &str) -> Option<Self>;
50
51 fn to_choice(self) -> &'static str;
53}
54
55#[derive(Debug, Clone)]
57pub enum MatchArgError {
58 InvalidType(SEXPTYPE),
60 InvalidLength(usize),
62 IsNa,
64 NoMatch {
66 input: String,
68 choices: &'static [&'static str],
70 },
71}
72
73impl std::fmt::Display for MatchArgError {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match self {
76 MatchArgError::InvalidType(ty) => {
77 write!(f, "match.arg: expected character or factor, got {:?}", ty)
78 }
79 MatchArgError::InvalidLength(len) => {
80 write!(f, "match.arg: expected length 1, got {}", len)
81 }
82 MatchArgError::IsNa => write!(f, "match.arg: input is NA"),
83 MatchArgError::NoMatch { input, choices } => {
84 write!(
85 f,
86 "'arg' should be one of {}, got {:?}",
87 choices
88 .iter()
89 .map(|c| format!("{:?}", c))
90 .collect::<Vec<_>>()
91 .join(", "),
92 input,
93 )
94 }
95 }
96 }
97}
98
99impl std::error::Error for MatchArgError {}
100
101impl From<MatchArgError> for crate::from_r::SexpError {
102 fn from(e: MatchArgError) -> Self {
103 crate::from_r::SexpError::InvalidValue(e.to_string())
104 }
105}
106
107pub fn escape_r_string(s: &str) -> String {
115 let mut out = String::with_capacity(s.len());
116 for ch in s.chars() {
117 match ch {
118 '\\' => out.push_str(r"\\"),
119 '"' => out.push_str(r#"\""#),
120 '\n' => out.push_str(r"\n"),
121 '\r' => out.push_str(r"\r"),
122 '\t' => out.push_str(r"\t"),
123 c => out.push(c),
124 }
125 }
126 out
127}
128
129pub fn choices_sexp<T: MatchArg>() -> SEXP {
134 let choices = <T as MatchArg>::CHOICES;
135 unsafe {
136 let n = choices.len();
137 let vec = ffi::Rf_allocVector(SEXPTYPE::STRSXP, n as ffi::R_xlen_t);
138 ffi::Rf_protect(vec);
139 for (i, s) in choices.iter().enumerate() {
140 let charsxp = if s.is_empty() {
141 SEXP::blank_string()
142 } else {
143 SEXP::charsxp(s)
144 };
145 vec.set_string_elt(i as ffi::R_xlen_t, charsxp);
146 }
147 ffi::Rf_unprotect(1);
148 vec
149 }
150}
151
152fn sexp_err_to_match_arg_err(e: SexpError) -> MatchArgError {
158 match e {
159 SexpError::Type(t) => MatchArgError::InvalidType(t.actual),
160 SexpError::Length(l) => MatchArgError::InvalidLength(l.actual),
161 other => unreachable!("unexpected SexpError from string conversion: {other}"),
162 }
163}
164
165fn factor_elt_to_choice<T: MatchArg>(sexp: SEXP) -> Result<T, MatchArgError> {
167 let len = sexp.len();
168 if len != 1 {
169 return Err(MatchArgError::InvalidLength(len));
170 }
171 let idx = sexp.integer_elt(0);
172 if idx == i32::MIN {
173 return Err(MatchArgError::IsNa);
175 }
176 let levels = sexp.get_levels();
177 let level_idx = (idx - 1) as ffi::R_xlen_t;
179 if level_idx < 0 || level_idx >= levels.len() as ffi::R_xlen_t {
180 return Err(MatchArgError::NoMatch {
181 input: format!("<factor index {}>", idx),
182 choices: <T as MatchArg>::CHOICES,
183 });
184 }
185 let charsxp = levels.string_elt(level_idx);
186 match_choice::<T>(unsafe { charsxp_to_str(charsxp) })
188}
189
190pub fn match_arg_from_sexp<T: MatchArg>(sexp: SEXP) -> Result<T, MatchArgError> {
194 if sexp.type_of() == SEXPTYPE::NILSXP {
196 return T::from_choice(<T as MatchArg>::CHOICES[0]).ok_or_else(|| MatchArgError::NoMatch {
197 input: String::new(),
198 choices: <T as MatchArg>::CHOICES,
199 });
200 }
201
202 if sexp.is_factor() {
204 return factor_elt_to_choice::<T>(sexp);
205 }
206
207 let input = <Option<&'static str> as TryFromSexp>::try_from_sexp(sexp)
210 .map_err(sexp_err_to_match_arg_err)?
211 .ok_or(MatchArgError::IsNa)?;
212
213 match_choice::<T>(input)
214}
215
216fn match_choice<T: MatchArg>(input: &str) -> Result<T, MatchArgError> {
218 if let Some(val) = T::from_choice(input) {
220 return Ok(val);
221 }
222
223 let mut matches: Vec<(usize, &'static str)> = Vec::new();
225 for (i, choice) in <T as MatchArg>::CHOICES.iter().enumerate() {
226 if choice.starts_with(input) {
227 matches.push((i, choice));
228 }
229 }
230
231 match matches.len() {
232 1 => T::from_choice(matches[0].1).ok_or(MatchArgError::NoMatch {
233 input: input.to_string(),
234 choices: <T as MatchArg>::CHOICES,
235 }),
236 _ => Err(MatchArgError::NoMatch {
237 input: input.to_string(),
238 choices: <T as MatchArg>::CHOICES,
239 }),
240 }
241}
242
243pub fn match_arg_vec_into_sexp<T: MatchArg>(values: Vec<T>) -> SEXP {
250 unsafe {
251 let n = values.len();
252 let vec = ffi::Rf_allocVector(SEXPTYPE::STRSXP, n as ffi::R_xlen_t);
253 ffi::Rf_protect(vec);
254 for (i, v) in values.into_iter().enumerate() {
255 let s = v.to_choice();
256 let charsxp = if s.is_empty() {
257 SEXP::blank_string()
258 } else {
259 SEXP::charsxp(s)
260 };
261 vec.set_string_elt(i as ffi::R_xlen_t, charsxp);
262 }
263 ffi::Rf_unprotect(1);
264 vec
265 }
266}
267
268impl<T: MatchArg> IntoR for Vec<T> {
276 type Error = std::convert::Infallible;
277
278 fn try_into_sexp(self) -> Result<SEXP, Self::Error> {
279 Ok(self.into_sexp())
280 }
281
282 unsafe fn try_into_sexp_unchecked(self) -> Result<SEXP, Self::Error> {
283 self.try_into_sexp()
284 }
285
286 fn into_sexp(self) -> SEXP {
287 match_arg_vec_into_sexp(self)
288 }
289}
290
291pub fn match_arg_vec_from_sexp<T: MatchArg>(sexp: SEXP) -> Result<Vec<T>, MatchArgError> {
302 if sexp.type_of() == SEXPTYPE::NILSXP {
304 return <T as MatchArg>::CHOICES
305 .iter()
306 .map(|c| match_choice::<T>(c))
307 .collect();
308 }
309
310 <Vec<Option<&'static str>> as TryFromSexp>::try_from_sexp(sexp)
313 .map_err(sexp_err_to_match_arg_err)?
314 .into_iter()
315 .map(|opt| opt.ok_or(MatchArgError::IsNa).and_then(match_choice::<T>))
316 .collect()
317}
318
319#[cfg(test)]
320mod tests {
321 use super::escape_r_string;
322
323 #[test]
324 fn escapes_backslash_and_quote() {
325 assert_eq!(escape_r_string(r#"say "hi""#), r#"say \"hi\""#);
326 assert_eq!(escape_r_string(r"c:\path"), r"c:\\path");
327 }
328
329 #[test]
330 fn escapes_control_characters() {
331 assert_eq!(escape_r_string("line1\nline2"), r"line1\nline2");
332 assert_eq!(escape_r_string("tab\there"), r"tab\there");
333 assert_eq!(escape_r_string("cr\rlf"), r"cr\rlf");
334 }
335
336 #[test]
337 fn passes_through_plain_strings() {
338 assert_eq!(escape_r_string("Fast"), "Fast");
339 assert_eq!(escape_r_string("it's"), "it's");
340 assert_eq!(escape_r_string(""), "");
341 }
342}