Skip to main content

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    ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("invalid {0}: {1}", T::LABEL, err))
    })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}