Repository: extendr/extendr
Pull request #249 (2021-08-02 20:14:32)

More try_from conversions
This PR covers i32, f64, Bool and u8 types in vectors and slices.

```
        assert_eq!(<Vec::<i32>>::try_from(Robj::from(1)), Ok(vec![1]));
        assert_eq!(<Vec::<f64>>::try_from(Robj::from(1.)), Ok(vec![1.]));
        assert_eq!(<Vec::<Bool>>::try_from(Robj::from(TRUE)), Ok(vec![TRUE]));
        assert_eq!(<Vec::<u8>>::try_from(Robj::from(0_u8)), Ok(vec![0_u8]));

        assert_eq!(<&[i32]>::try_from(Robj::from(1)), Ok(&[1][..]));
        assert_eq!(<&[f64]>::try_from(Robj::from(1.)), Ok(&[1.][..]));
        assert_eq!(<&[Bool]>::try_from(Robj::from(TRUE)), Ok(&[TRUE][..]));
        assert_eq!(<&[u8]>::try_from(Robj::from(0_u8)), Ok(&[0_u8][..]));
```

yutannihilation: > We need some sort of option that allows a Rust function to accept either integer or float vectors as input and use them as either floats or integers, with auto-conversion as appropriate.

I agree. I think we might need to introduce some wrapper like `Nullable`. Or, if it's not necessarily on Rust's side, call `as.xxx()` function in the wrapper R function?

Patch:
diff --git a/extendr-api/src/lib.rs b/extendr-api/src/lib.rs
index f97198359..8524d619d 100644
--- a/extendr-api/src/lib.rs
+++ b/extendr-api/src/lib.rs
@@ -655,7 +655,7 @@ mod tests {
                 wrap__robjtype(Robj::from(1).get());
 
                 // General integer types.
-                assert_eq!(new_owned(wrap__return_u8()), Robj::from(123));
+                assert_eq!(new_owned(wrap__return_u8()), Robj::from(123_u8));
                 assert_eq!(new_owned(wrap__return_u16()), Robj::from(123));
                 assert_eq!(new_owned(wrap__return_u32()), Robj::from(123.));
                 assert_eq!(new_owned(wrap__return_u64()), Robj::from(123.));
diff --git a/extendr-api/src/robj/into_robj.rs b/extendr-api/src/robj/into_robj.rs
index 8bb389f19..f7cbf4267 100644
--- a/extendr-api/src/robj/into_robj.rs
+++ b/extendr-api/src/robj/into_robj.rs
@@ -199,9 +199,28 @@ macro_rules! impl_integer_tvv {
 impl_integer_tvv!(i8);
 impl_integer_tvv!(i16);
 impl_integer_tvv!(i32);
-impl_integer_tvv!(u8);
 impl_integer_tvv!(u16);
 
+impl ToVectorValue for u8 {
+    fn sexptype() -> SEXPTYPE {
+        RAWSXP
+    }
+
+    fn to_raw(&self) -> u8 {
+        *self as u8
+    }
+}
+
+impl ToVectorValue for &u8 {
+    fn sexptype() -> SEXPTYPE {
+        RAWSXP
+    }
+
+    fn to_raw(&self) -> u8 {
+        **self as u8
+    }
+}
+
 macro_rules! impl_str_tvv {
     ($t: ty) => {
         impl ToVectorValue for $t {
@@ -355,6 +374,12 @@ where
                         SET_STRING_ELT(sexp, i as isize, v.to_sexp());
                     }
                 }
+                RAWSXP => {
+                    let ptr = RAW(sexp);
+                    for (i, v) in iter.enumerate() {
+                        *ptr.add(i) = v.to_raw();
+                    }
+                }
                 _ => {
                     panic!("unexpected SEXPTYPE in collect_robj");
                 }
diff --git a/extendr-api/src/robj/try_from_robj.rs b/extendr-api/src/robj/try_from_robj.rs
index af67ee50b..37a90b960 100644
--- a/extendr-api/src/robj/try_from_robj.rs
+++ b/extendr-api/src/robj/try_from_robj.rs
@@ -7,6 +7,7 @@ macro_rules! impl_try_from_scalar_integer {
         impl TryFrom<Robj> for $t {
             type Error = Error;
 
+            /// Convert a numeric object to an integer value.
             fn try_from(robj: Robj) -> Result<Self> {
                 // Check if the value is a scalar
                 match robj.len() {
@@ -57,6 +58,7 @@ macro_rules! impl_try_from_scalar_real {
         impl TryFrom<Robj> for $t {
             type Error = Error;
 
+            /// Convert a numeric object to a real value.
             fn try_from(robj: Robj) -> Result<Self> {
                 // Check if the value is a scalar
                 match robj.len() {
@@ -100,6 +102,8 @@ impl_try_from_scalar_real!(f64);
 impl TryFrom<Robj> for Bool {
     type Error = Error;
 
+    /// Convert an LGLSXP object into a Bool (tri-state boolean).
+    /// Use `value.is_na()` to detect NA values.
     fn try_from(robj: Robj) -> Result<Self> {
         if let Some(v) = robj.as_logical_slice() {
             match v.len() {
@@ -116,6 +120,8 @@ impl TryFrom<Robj> for Bool {
 impl TryFrom<Robj> for bool {
     type Error = Error;
 
+    /// Convert an LGLSXP object into a boolean.
+    /// NAs are not allowed.
     fn try_from(robj: Robj) -> Result<Self> {
         if robj.is_na() {
             Err(Error::MustNotBeNA(robj))
@@ -128,6 +134,8 @@ impl TryFrom<Robj> for bool {
 impl TryFrom<Robj> for &str {
     type Error = Error;
 
+    /// Convert a scalar STRSXP object into a string slice.
+    /// NAs are not allowed.
     fn try_from(robj: Robj) -> Result<Self> {
         if robj.is_na() {
             return Err(Error::MustNotBeNA(robj));
@@ -149,6 +157,9 @@ impl TryFrom<Robj> for &str {
 impl TryFrom<Robj> for String {
     type Error = Error;
 
+    /// Convert an scalar STRSXP object into a String.
+    /// Note: Unless you plan to store the result, use a string slice instead.
+    /// NAs are not allowed.
     fn try_from(robj: Robj) -> Result<Self> {
         <&str>::try_from(robj).map(|s| s.to_string())
     }
@@ -157,6 +168,9 @@ impl TryFrom<Robj> for String {
 impl TryFrom<Robj> for Vec<i32> {
     type Error = Error;
 
+    /// Convert an INTSXP object into a vector of i32 (integer).
+    /// Note: Unless you plan to store the result, use a slice instead.
+    /// Use `value.is_na()` to detect NA values.
     fn try_from(robj: Robj) -> Result<Self> {
         if let Some(v) = robj.as_integer_slice() {
             Ok(Vec::from(v))
@@ -169,6 +183,9 @@ impl TryFrom<Robj> for Vec<i32> {
 impl TryFrom<Robj> for Vec<f64> {
     type Error = Error;
 
+    /// Convert a REALSXP object into a vector of f64 (double precision floating point).
+    /// Note: Unless you plan to store the result, use a slice instead.
+    /// Use `value.is_na()` to detect NA values.
     fn try_from(robj: Robj) -> Result<Self> {
         if let Some(v) = robj.as_real_slice() {
             Ok(Vec::from(v))
@@ -178,9 +195,40 @@ impl TryFrom<Robj> for Vec<f64> {
     }
 }
 
+impl TryFrom<Robj> for Vec<Bool> {
+    type Error = Error;
+
+    /// Convert a LGLSXP object into a vector of Bool (tri-state booleans).
+    /// Note: Unless you plan to store the result, use a slice instead.
+    /// Use `value.is_na()` to detect NA values.
+    fn try_from(robj: Robj) -> Result<Self> {
+        if let Some(v) = robj.as_logical_slice() {
+            Ok(Vec::from(v))
+        } else {
+            Err(Error::ExpectedInteger(robj))
+        }
+    }
+}
+
+impl TryFrom<Robj> for Vec<u8> {
+    type Error = Error;
+
+    /// Convert a RAWSXP object into a vector of bytes.
+    /// Note: Unless you plan to store the result, use a slice instead.
+    fn try_from(robj: Robj) -> Result<Self> {
+        if let Some(v) = robj.as_raw_slice() {
+            Ok(Vec::from(v))
+        } else {
+            Err(Error::ExpectedRaw(robj))
+        }
+    }
+}
+
 impl TryFrom<Robj> for Vec<String> {
     type Error = Error;
 
+    /// Convert a STRSXP object into a vector of `String`s.
+    /// Note: Unless you plan to store the result, use a slice instead.
     fn try_from(robj: Robj) -> Result<Self> {
         if let Some(iter) = robj.as_str_iter() {
             // check for NA's in the string vector
@@ -195,25 +243,13 @@ impl TryFrom<Robj> for Vec<String> {
     }
 }
 
-// The following fails because T could be Option<U>
-//
-// impl<T : TryFrom<Robj>> TryFrom<Robj> for Option<T> {
-//     type Error = Error;
-
-//     fn try_from(robj: Robj) -> Result<Self> {
-//         if robj.is_na() {
-//             Ok(None)
-//         } else {
-//             Ok(Some(T::try_from(robj)?))
-//         }
-//     }
-// }
-
 macro_rules! impl_option {
     ($type : ty) => {
         impl TryFrom<Robj> for Option<$type> {
             type Error = Error;
 
+            /// Convert a scalar object that may be NA type to an `Option` of a corresponding type.
+            /// Returns `None` if the scalar is NA.
             fn try_from(robj: Robj) -> Result<Self> {
                 if robj.is_na() {
                     Ok(None)
@@ -243,9 +279,53 @@ impl_option!(Vec<i32>);
 impl_option!(Vec<f64>);
 impl_option!(Vec<String>);
 
+impl TryFrom<Robj> for &[i32] {
+    type Error = Error;
+
+    /// Convert an INTSXP object into a slice of i32 (integer).
+    /// Use `value.is_na()` to detect NA values.
+    fn try_from(robj: Robj) -> Result<Self> {
+        robj.as_typed_slice()
+            .ok_or_else(|| Error::ExpectedInteger(robj))
+    }
+}
+
+impl TryFrom<Robj> for &[Bool] {
+    type Error = Error;
+
+    /// Convert a LGLSXP object into a slice of Bool (tri-state booleans).
+    /// Use `value.is_na()` to detect NA values.
+    fn try_from(robj: Robj) -> Result<Self> {
+        robj.as_typed_slice()
+            .ok_or_else(|| Error::ExpectedLogical(robj))
+    }
+}
+
+impl TryFrom<Robj> for &[u8] {
+    type Error = Error;
+
+    /// Convert a RAWSXP object into a slice of bytes.
+    fn try_from(robj: Robj) -> Result<Self> {
+        robj.as_typed_slice()
+            .ok_or_else(|| Error::ExpectedRaw(robj))
+    }
+}
+
+impl TryFrom<Robj> for &[f64] {
+    type Error = Error;
+
+    /// Convert a REALSXP object into a slice of f64 (double precision floating point).
+    /// Use `value.is_na()` to detect NA values.
+    fn try_from(robj: Robj) -> Result<Self> {
+        robj.as_typed_slice()
+            .ok_or_else(|| Error::ExpectedReal(robj))
+    }
+}
+
 impl TryFrom<Robj> for HashMap<String, Robj> {
     type Error = Error;
 
+    /// Convert a named VECSXP object into a hashmap of `String`-value pairs.
     fn try_from(robj: Robj) -> Result<Self> {
         if let Some(iter) = robj.as_list().map(|l| l.iter()) {
             Ok(iter
@@ -260,6 +340,7 @@ impl TryFrom<Robj> for HashMap<String, Robj> {
 impl TryFrom<Robj> for HashMap<&str, Robj> {
     type Error = Error;
 
+    /// Convert a named VECSXP object into a hashmap of `&str`-value pairs.
     fn try_from(robj: Robj) -> Result<Self> {
         if let Some(iter) = robj.as_list().map(|l| l.iter()) {
             Ok(iter.map(|(k, v)| (k, v)).collect::<HashMap<&str, Robj>>())
diff --git a/extendr-api/src/wrapper/environment.rs b/extendr-api/src/wrapper/environment.rs
index 9e5695526..7deebaf71 100644
--- a/extendr-api/src/wrapper/environment.rs
+++ b/extendr-api/src/wrapper/environment.rs
@@ -1,5 +1,4 @@
 use super::*;
-use crate as extendr_api;
 
 #[derive(Debug, PartialEq, Clone)]
 pub struct Environment {


Test patch:
diff --git a/extendr-api/src/robj/tests.rs b/extendr-api/src/robj/tests.rs
index 510875db6..bef7df8fb 100644
--- a/extendr-api/src/robj/tests.rs
+++ b/extendr-api/src/robj/tests.rs
@@ -153,10 +153,26 @@ fn test_try_from_robj() {
 
         assert_eq!(<Vec::<i32>>::try_from(Robj::from(1)), Ok(vec![1]));
         assert_eq!(<Vec::<f64>>::try_from(Robj::from(1.)), Ok(vec![1.]));
+        assert_eq!(<Vec::<Bool>>::try_from(Robj::from(TRUE)), Ok(vec![TRUE]));
+        assert_eq!(<Vec::<u8>>::try_from(Robj::from(0_u8)), Ok(vec![0_u8]));
+
+        assert_eq!(<&[i32]>::try_from(Robj::from(1)), Ok(&[1][..]));
+        assert_eq!(<&[f64]>::try_from(Robj::from(1.)), Ok(&[1.][..]));
+        assert_eq!(<&[Bool]>::try_from(Robj::from(TRUE)), Ok(&[TRUE][..]));
+        assert_eq!(<&[u8]>::try_from(Robj::from(0_u8)), Ok(&[0_u8][..]));
+
+        // Note the Vec<> cases use the same logic as the slices.
+        assert_eq!(<&[i32]>::try_from(Robj::from(1.0)), Err(Error::ExpectedInteger(r!(1.0))));
+        assert_eq!(<&[f64]>::try_from(Robj::from(1)), Err(Error::ExpectedReal(r!(1))));
+        assert_eq!(<&[Bool]>::try_from(Robj::from(())), Err(Error::ExpectedLogical(r!(()))));
+        assert_eq!(<&[u8]>::try_from(Robj::from(())), Err(Error::ExpectedRaw(r!(()))));
 
         let hello = Robj::from("hello");
         assert_eq!(<&str>::try_from(hello), Ok("hello"));
 
+        let hello = Robj::from("hello");
+        assert_eq!(<String>::try_from(hello), Ok("hello".into()));
+
         // conversion from a vector to a scalar value
         let robj = Robj::from(vec![].as_slice() as &[i32]);
         assert_eq!(