Repository: shepmaster/snafu (Rust)

Issue:
`ensure` doesn't play well with opaque errors
Combining opaque error types with `ensure` doesn't really work well.

`fail` returns a `Result<T, Error>`, and with opaque errors, `Error` will be the private `ExampleInnerError`, so the final type is incompatible with `Result<T, ExamplePublicError>`

`ensure` merely returns what `fail` does, which as far as I can tell, makes it unusable in a public API.

Maybe it'd make sense for `ensure` to do something like `return $context_selector.fail().map_err(|e|.into());` instead?

Example

```rust
#[derive(Debug, Snafu)]
pub struct ExamplePublicError(ExampleInnerError);

#[derive(Debug, Snafu)]
enum ExampleInnerError {
    ExampleError {},
}

type ExampleResult<T, E = ExamplePublicError> = std::result::Result<T, E>;

pub fn example() -> ExampleResult<()> {
    ensure!(false, ExampleError);
    return ExampleError.fail();
	// return ExampleError.fail().map_err(|e| e.into()); // Works
}
```

Output:

```
error[E0308]: mismatched types
   --> src/lib.rs:154:5
    |
153 | pub fn example() -> ExampleResult<()> {
    |                     ----------------- expected `std::result::Result<(), crate::ExamplePublicError>` because of return type
154 |     ensure!(false, ExampleError);
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `crate::ExamplePublicError`, found enum `crate::ExampleInnerError`
    |
    = note: expected type `std::result::Result<(), crate::ExamplePublicError>`
               found type `std::result::Result<_, crate::ExampleInnerError>`
    = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)
```


Discussion:
Thank you for the report and the awesome, concise example.

Truth be told, I don't think I thought about using `ensure` with an opaque error type at all! (oops)



> Maybe it'd make sense for `ensure` to do something like `return $context_selector.fail().map_err(|e|.into());` instead?

This certainly seems like a good start. The only thing tickling the back of my head is some worry around ambiguities resolving the types for `Into`, but as far as I can see the starting and ending types are both pretty concretely defined. I think it's worth giving this a shot!

Pedantically, I'd recommend trying `return $context_selector.fail().map_err(core::convert::Into
::into);` to be a bit broader and avoid creating the closure.



Patch:
diff --git a/src/lib.rs b/src/lib.rs
index 93e375c2..6a327439 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -151,7 +151,7 @@ use std::error;
 macro_rules! ensure {
     ($predicate:expr, $context_selector:expr) => {
         if !$predicate {
-            return $context_selector.fail();
+            return $context_selector.fail().map_err(core::convert::Into::into);
         }
     };
 }


Test patch:
diff --git a/tests/opaque.rs b/tests/opaque.rs
index 3f8badae..1455c6df 100644
--- a/tests/opaque.rs
+++ b/tests/opaque.rs
@@ -1,5 +1,5 @@
 mod inner {
-    use snafu::Snafu;
+    use snafu::{ensure, Snafu};
 
     #[derive(Debug, Snafu)]
     pub struct Error(InnerError);
@@ -8,6 +8,16 @@ mod inner {
         Ok(a()? + b()?)
     }
 
+    pub fn not_positive(value: i32) -> Result<i32, Error> {
+        ensure!(value < 1, TooBig { count: value });
+        Ok(value)
+    }
+
+    pub fn boxed_inner(value: i32) -> Result<i32, Box<dyn std::error::Error>> {
+        ensure!(value < 1, TooBig { count: value });
+        Ok(value)
+    }
+
     #[derive(Debug, Snafu)]
     enum InnerError {
         #[snafu(display = r#"("The value {} is too big", count)"#)]
@@ -30,3 +40,19 @@ fn implements_error() {
     let e = inner::api().unwrap_err();
     assert!(e.to_string().contains("too big"));
 }
+
+#[test]
+fn ensure_opaque() {
+    assert!(inner::not_positive(-1).is_ok());
+
+    let e = inner::not_positive(2).unwrap_err();
+    assert!(e.to_string().contains("too big"));
+}
+
+#[test]
+fn ensure_boxed() {
+    assert!(inner::boxed_inner(-1).is_ok());
+
+    let e = inner::boxed_inner(2).unwrap_err();
+    assert!(e.to_string().contains("too big"));
+}