miniextendr_api/
match_arg.rs1use crate::ffi::{self, SEXP, SEXPTYPE, SexpExt};
31
32pub trait MatchArg: Sized + Copy + 'static {
39 const CHOICES: &'static [&'static str];
43
44 fn from_choice(choice: &str) -> Option<Self>;
48
49 fn to_choice(self) -> &'static str;
51}
52
53#[derive(Debug, Clone)]
55pub enum MatchArgError {
56 InvalidType(SEXPTYPE),
58 InvalidLength(usize),
60 IsNa,
62 NoMatch {
64 input: String,
66 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
105pub 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
128pub fn match_arg_from_sexp<T: MatchArg>(sexp: SEXP) -> Result<T, MatchArgError> {
133 let actual_type = sexp.type_of();
134
135 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 SEXPTYPE::INTSXP => {
152 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 return Err(MatchArgError::IsNa);
165 }
166 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 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 if let Some(val) = T::from_choice(&input) {
193 return Ok(val);
194 }
195
196 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 Err(MatchArgError::NoMatch {
216 input,
217 choices: <T as MatchArg>::CHOICES,
218 })
219 }
220 }
221}