1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! GraphQL support for [`anyhow::Error`].
//!
//! # Example
//!
//! ```rust
//! # use std::backtrace::Backtrace;
//! use anyhow::anyhow;
//! # use juniper::graphql_object;
//!
//! struct Root;
//!
//! #[graphql_object]
//! impl Root {
//!     fn err() -> anyhow::Result<i32> {
//!         Err(anyhow!("errored!"))
//!     }
//! }
//! ```
//!
//! # Backtrace
//!
//! Backtrace is supported in the same way as [`anyhow`] crate does:
//! > If using the nightly channel, or stable with `features = ["backtrace"]`, a backtrace is
//! > captured and printed with the error if the underlying error type does not already provide its
//! > own. In order to see backtraces, they must be enabled through the environment variables
//! > described in [`std::backtrace`]:
//! > - If you want panics and errors to both have backtraces, set `RUST_BACKTRACE=1`;
//! > - If you want only errors to have backtraces, set `RUST_LIB_BACKTRACE=1`;
//! > - If you want only panics to have backtraces, set `RUST_BACKTRACE=1` and
//! >   `RUST_LIB_BACKTRACE=0`.

use crate::{FieldError, IntoFieldError, ScalarValue, Value};

impl<S: ScalarValue> IntoFieldError<S> for anyhow::Error {
    fn into_field_error(self) -> FieldError<S> {
        #[cfg(any(nightly, feature = "backtrace"))]
        let extensions = {
            let backtrace = self.backtrace().to_string();
            if backtrace == "disabled backtrace" {
                Value::Null
            } else {
                let mut obj = crate::value::Object::with_capacity(1);
                _ = obj.add_field(
                    "backtrace",
                    Value::List(
                        backtrace
                            .split('\n')
                            .map(|line| Value::Scalar(line.to_owned().into()))
                            .collect(),
                    ),
                );
                Value::Object(obj)
            }
        };
        #[cfg(not(any(nightly, feature = "backtrace")))]
        let extensions = Value::Null;

        FieldError::new(self, extensions)
    }
}

#[cfg(test)]
mod test {
    use std::env;

    use anyhow::anyhow;
    use serial_test::serial;

    use crate::{
        execute, graphql_object, graphql_value, graphql_vars, parser::SourcePosition,
        EmptyMutation, EmptySubscription, RootNode,
    };

    #[tokio::test]
    #[serial]
    async fn simple() {
        struct Root;

        #[graphql_object]
        impl Root {
            fn err() -> anyhow::Result<i32> {
                Err(anyhow!("errored!"))
            }
        }

        let prev_env = env::var("RUST_BACKTRACE").ok();
        env::set_var("RUST_BACKTRACE", "1");

        const DOC: &str = r#"{
            err
        }"#;

        let schema = RootNode::new(
            Root,
            EmptyMutation::<()>::new(),
            EmptySubscription::<()>::new(),
        );

        let res = execute(DOC, None, &schema, &graphql_vars! {}, &()).await;

        assert!(res.is_ok(), "failed: {:?}", res.unwrap_err());

        let (val, errs) = res.unwrap();

        assert_eq!(val, graphql_value!(null));
        assert_eq!(errs.len(), 1, "too many errors: {errs:?}");

        let err = errs.first().unwrap();

        assert_eq!(*err.location(), SourcePosition::new(14, 1, 12));
        assert_eq!(err.path(), &["err"]);

        let err = err.error();

        assert_eq!(err.message(), "errored!");
        #[cfg(not(any(nightly, feature = "backtrace")))]
        assert_eq!(err.extensions(), &graphql_value!(null));
        #[cfg(any(nightly, feature = "backtrace"))]
        assert_eq!(
            err.extensions()
                .as_object_value()
                .map(|ext| ext.contains_field("backtrace")),
            Some(true),
            "no `backtrace` in extensions: {err:?}",
        );

        if let Some(val) = prev_env {
            env::set_var("RUST_BACKTRACE", val);
        }
    }
}