miniextendr_api/from_r/str_parse.rs
1//! Conversions for types parsed from an R string: [`ParseRStr`],
2//! [`ParsedRStr`] and [`try_from_sexp_via_str_parse!`](crate::try_from_sexp_via_str_parse).
3//!
4//! A string-parsed type `T` gets its four conversions (`T`, `Option<T>`,
5//! `Vec<T>`, `Vec<Option<T>>`) from the [`ParsedRStr<T>`] impls in this module.
6//! The container shapes reach `T` through the
7//! [`TryFromSexpElement`](crate::TryFromSexpElement) blankets, with
8//! `ParsedRStr<T>` as the inner type, so they compile in any crate (#1766).
9
10use super::{BatchedErrors, SexpError, SexpNaError, TryFromSexp};
11use crate::{SEXP, SEXPTYPE};
12
13/// Parse hook for a type read from an R character vector.
14///
15/// Implemented by [`try_from_sexp_via_str_parse!`](crate::try_from_sexp_via_str_parse),
16/// together with the `TryFromSexp` and
17/// [`TryFromSexpElement`](crate::TryFromSexpElement) impls that give the type
18/// its four conversions. Use the macro rather than implementing this by hand.
19pub trait ParseRStr: Sized {
20 /// What a failed parse calls the value: `invalid <LABEL>: <error>`.
21 const LABEL: &'static str;
22
23 /// Parse one string (never `NA`).
24 ///
25 /// The error is formatted only when it is reported, so a long vector
26 /// that fails everywhere builds at most the ten messages a batched error
27 /// lists.
28 fn parse_r_str(s: &str) -> Result<Self, impl std::fmt::Display>;
29}
30
31/// A value of the string-parsed type `T`: the inner type of `T`'s
32/// [`TryFromSexpElement`](crate::TryFromSexpElement) impl.
33///
34/// `Vec<T>`, `Option<T>` and `Vec<Option<T>>` read the same container of
35/// `ParsedRStr<T>`, whose conversions here apply the NA policy and batch the
36/// element errors. Plumbing for
37/// [`try_from_sexp_via_str_parse!`](crate::try_from_sexp_via_str_parse).
38pub struct ParsedRStr<T>(pub T);
39
40/// `invalid <label>: <error>`, the reason a parse failure gives.
41fn invalid<T: ParseRStr>(err: impl std::fmt::Display) -> String {
42 format!("invalid {}: {err}", T::LABEL)
43}
44
45/// `NA_character_` / `NULL` → `SexpError::Na`; a parse failure →
46/// `InvalidValue("invalid <label>: <error>")`.
47impl<T: ParseRStr> TryFromSexp for ParsedRStr<T> {
48 type Error = SexpError;
49 const CHARACTER_ONLY: bool = true;
50
51 fn try_from_sexp(sexp: SEXP) -> Result<Self, SexpError> {
52 let s: Option<String> = TryFromSexp::try_from_sexp(sexp)?;
53 let s = s.ok_or(SexpError::Na(SexpNaError {
54 sexp_type: SEXPTYPE::STRSXP,
55 }))?;
56 T::parse_r_str(&s)
57 .map(ParsedRStr)
58 .map_err(|e| SexpError::InvalidValue(invalid::<T>(e)))
59 }
60}
61
62/// `NA_character_` / `NULL` → `None`; a parse failure as for the scalar.
63impl<T: ParseRStr> TryFromSexp for Option<ParsedRStr<T>> {
64 type Error = SexpError;
65 const CHARACTER_ONLY: bool = true;
66
67 fn try_from_sexp(sexp: SEXP) -> Result<Self, SexpError> {
68 let s: Option<String> = TryFromSexp::try_from_sexp(sexp)?;
69 match s {
70 None => Ok(None),
71 Some(s) => T::parse_r_str(&s)
72 .map(|v| Some(ParsedRStr(v)))
73 .map_err(|e| SexpError::InvalidValue(invalid::<T>(e))),
74 }
75 }
76}
77
78/// `NA` elements and parse failures are collected across the whole vector
79/// into one batched `InvalidValue`, each reason followed by the
80/// 1-based positions that failed with it:
81/// `NA is not allowed (element 2); invalid <label>: <error> (elements 3, 5)`.
82impl<T: ParseRStr> TryFromSexp for Vec<ParsedRStr<T>> {
83 type Error = SexpError;
84 const CHARACTER_ONLY: bool = true;
85
86 fn try_from_sexp(sexp: SEXP) -> Result<Self, SexpError> {
87 let values: Vec<Option<String>> = TryFromSexp::try_from_sexp(sexp)?;
88 let mut result = Vec::with_capacity(values.len());
89 let mut errors = BatchedErrors::default();
90 for (i, s) in values.into_iter().enumerate() {
91 match s {
92 None => errors.push(i, || "NA is not allowed".to_string()),
93 Some(s) => match T::parse_r_str(&s) {
94 Ok(v) => result.push(ParsedRStr(v)),
95 Err(e) => errors.push(i, || invalid::<T>(e)),
96 },
97 }
98 }
99 if errors.is_empty() {
100 Ok(result)
101 } else {
102 Err(errors.into_element_error())
103 }
104 }
105}
106
107/// `NA` elements → `None`; parse failures batch as for `Vec<T>`.
108impl<T: ParseRStr> TryFromSexp for Vec<Option<ParsedRStr<T>>> {
109 type Error = SexpError;
110 const CHARACTER_ONLY: bool = true;
111
112 fn try_from_sexp(sexp: SEXP) -> Result<Self, SexpError> {
113 let values: Vec<Option<String>> = TryFromSexp::try_from_sexp(sexp)?;
114 let mut result = Vec::with_capacity(values.len());
115 let mut errors = BatchedErrors::default();
116 for (i, s) in values.into_iter().enumerate() {
117 match s {
118 None => result.push(None),
119 Some(s) => match T::parse_r_str(&s) {
120 Ok(v) => result.push(Some(ParsedRStr(v))),
121 Err(e) => errors.push(i, || invalid::<T>(e)),
122 },
123 }
124 }
125 if errors.is_empty() {
126 Ok(result)
127 } else {
128 Err(errors.into_element_error())
129 }
130 }
131}
132
133/// Give a type parsed from an R string its four `TryFromSexp` conversions:
134/// `T`, `Option<T>`, `Vec<T>` and `Vec<Option<T>>`.
135///
136/// Expands in any crate. It implements
137/// [`ParseRStr`] (the parse step),
138/// `TryFromSexp` for the type, and
139/// [`TryFromSexpElement`](crate::TryFromSexpElement), through which the
140/// container blankets in `miniextendr-api` convert `Option<T>`, `Vec<T>` and
141/// `Vec<Option<T>>`. These are all impls on your own type, so the orphan rule
142/// allows them; the container impls themselves can only live in
143/// `miniextendr-api` (#1766).
144///
145/// The built-in uuid, url, regex and num-bigint conversions use this macro, so
146/// a type given it reads R input the same way:
147///
148/// - `T`: `NA_character_` / `NULL` → `SexpError::Na`; a parse failure →
149/// `InvalidValue("invalid <label>: <err>")`.
150/// - `Option<T>`: `NA_character_` / `NULL` → `None`.
151/// - `Vec<T>`: `NA` elements and parse failures are collected across the
152/// whole vector into one batched `InvalidValue`, each reason followed by
153/// the 1-based positions that failed with it:
154/// `NA is not allowed (element 2); invalid <label>: <err> (elements 3, 5)`.
155/// The first 10 failures are listed and the remainder is summarized as
156/// `"and N more"`.
157/// - `Vec<Option<T>>`: `NA` elements → `None`; parse failures batch as above.
158///
159/// All four read only character input (`TryFromSexp::CHARACTER_ONLY`).
160///
161/// The parse body is a closure-style `|s| expr` where `s: &str`, returning
162/// `Result<T, E>` with `E: Display`.
163///
164/// ```ignore
165/// pub struct Slug(String);
166///
167/// impl std::str::FromStr for Slug {
168/// type Err = &'static str;
169/// fn from_str(s: &str) -> Result<Self, Self::Err> {
170/// if s.is_empty() || !s.chars().all(|c| c.is_ascii_lowercase() || c == '-') {
171/// return Err("expected lowercase letters and '-'");
172/// }
173/// Ok(Slug(s.to_owned()))
174/// }
175/// }
176///
177/// miniextendr_api::try_from_sexp_via_str_parse!(Slug, "slug", |s| s.parse::<Slug>());
178///
179/// #[miniextendr]
180/// pub fn slugs(x: Vec<Option<Slug>>) -> i32 { /* ... */ }
181/// ```
182///
183/// The type must not be generic, and the macro must not be used on a type
184/// that also derives `TryFromSexp`: both implement `TryFromSexpElement`.
185#[macro_export]
186macro_rules! try_from_sexp_via_str_parse {
187 ($ty:ty, $label:literal, |$s:ident| $parse:expr) => {
188 impl $crate::from_r::ParseRStr for $ty {
189 const LABEL: &'static str = $label;
190
191 fn parse_r_str($s: &str) -> ::core::result::Result<$ty, impl ::core::fmt::Display> {
192 $parse
193 }
194 }
195
196 impl $crate::from_r::TryFromSexp for $ty {
197 type Error = $crate::from_r::SexpError;
198 const CHARACTER_ONLY: bool = true;
199
200 fn try_from_sexp(
201 sexp: $crate::SEXP,
202 ) -> ::core::result::Result<$ty, $crate::from_r::SexpError> {
203 <$crate::from_r::ParsedRStr<$ty> as $crate::from_r::TryFromSexp>::try_from_sexp(
204 sexp,
205 )
206 .map(|parsed| parsed.0)
207 }
208 }
209
210 impl $crate::TryFromSexpElement for $ty {
211 type Inner = $crate::from_r::ParsedRStr<$ty>;
212
213 #[inline]
214 fn from_inner(inner: $crate::from_r::ParsedRStr<$ty>) -> $ty {
215 inner.0
216 }
217 }
218 };
219}