Skip to content

Commit 316b188

Browse files
authored
Provide anyhow crate integration (#1215, #988)
- implement `IntoFieldError` for `anyhow::Error` - add `anyhow` and `backtrace` Cargo features
1 parent 2215cd0 commit 316b188

File tree

6 files changed

+144
-2
lines changed

6 files changed

+144
-2
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ jobs:
9999
matrix:
100100
include:
101101
- { feature: <none>, crate: juniper }
102+
- { feature: anyhow, crate: juniper }
103+
- { feature: "anyhow,backtrace", crate: juniper }
102104
- { feature: bigdecimal, crate: juniper }
103105
- { feature: bson, crate: juniper }
104106
- { feature: chrono, crate: juniper }

juniper/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
6969
- `js` [Cargo feature] enabling `js-sys` and `wasm-bindgen` support for `wasm32-unknown-unknown` target. ([#1118], [#1147])
7070
- `LookAheadMethods::applies_for()` method. ([#1138], [#1145])
7171
- `LookAheadMethods::field_original_name()` and `LookAheadMethods::field_alias()` methods. ([#1199])
72+
- [`anyhow` crate] integration behind `anyhow` and `backtrace` [Cargo feature]s. ([#1215], [#988])
7273

7374
### Changed
7475

@@ -98,6 +99,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
9899
[#979]: /../../pull/979
99100
[#985]: /../../pull/985
100101
[#987]: /../../pull/987
102+
[#988]: /../../issues/988
101103
[#996]: /../../pull/996
102104
[#1000]: /../../issues/1000
103105
[#1001]: /../../pull/1001
@@ -137,6 +139,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
137139
[#1207]: /../../pull/1207
138140
[#1208]: /../../pull/1208
139141
[#1209]: /../../pull/1209
142+
[#1215]: /../../pull/1215
140143
[ba1ed85b]: /../../commit/ba1ed85b3c3dd77fbae7baf6bc4e693321a94083
141144
[CVE-2022-31173]: /../../security/advisories/GHSA-4rx6-g5vg-5f3j
142145

@@ -150,6 +153,7 @@ See [old CHANGELOG](/../../blob/juniper-v0.15.9/juniper/CHANGELOG.md).
150153

151154

152155

156+
[`anyhow` crate]: https://docs.rs/anyhow
153157
[`bigdecimal` crate]: https://docs.rs/bigdecimal
154158
[`bson` crate]: https://docs.rs/bson
155159
[`chrono` crate]: https://docs.rs/chrono

juniper/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ default = [
3131
"url",
3232
"uuid",
3333
]
34+
anyhow = ["dep:anyhow"]
35+
backtrace = ["anyhow?/backtrace"]
3436
bigdecimal = ["dep:bigdecimal", "dep:num-bigint", "dep:ryu"]
3537
bson = ["dep:bson"]
3638
chrono = ["dep:chrono"]
@@ -46,7 +48,7 @@ url = ["dep:url"]
4648
uuid = ["dep:uuid"]
4749

4850
[dependencies]
49-
anyhow = { version = "1.0.47", default-features = false, optional = true }
51+
anyhow = { version = "1.0.47", optional = true }
5052
async-trait = "0.1.39"
5153
bigdecimal = { version = "0.4", optional = true }
5254
bson = { version = "2.4", features = ["chrono-0_4"], optional = true }
@@ -81,6 +83,7 @@ bencher = "0.1.2"
8183
chrono = { version = "0.4.30", features = ["alloc"], default-features = false }
8284
pretty_assertions = "1.0.0"
8385
serde_json = "1.0.18"
86+
serial_test = "2.0"
8487
tokio = { version = "1.0", features = ["macros", "time", "rt-multi-thread"] }
8588

8689
[[bench]]

juniper/src/integrations/anyhow.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
//! GraphQL support for [`anyhow::Error`].
2+
//!
3+
//! # Example
4+
//!
5+
//! ```rust
6+
//! # use std::backtrace::Backtrace;
7+
//! use anyhow::anyhow;
8+
//! # use juniper::graphql_object;
9+
//!
10+
//! struct Root;
11+
//!
12+
//! #[graphql_object]
13+
//! impl Root {
14+
//! fn err() -> anyhow::Result<i32> {
15+
//! Err(anyhow!("errored!"))
16+
//! }
17+
//! }
18+
//! ```
19+
//!
20+
//! # Backtrace
21+
//!
22+
//! Backtrace is supported in the same way as [`anyhow`] crate does:
23+
//! > If using the nightly channel, or stable with `features = ["backtrace"]`, a backtrace is
24+
//! > captured and printed with the error if the underlying error type does not already provide its
25+
//! > own. In order to see backtraces, they must be enabled through the environment variables
26+
//! > described in [`std::backtrace`]:
27+
//! > - If you want panics and errors to both have backtraces, set `RUST_BACKTRACE=1`;
28+
//! > - If you want only errors to have backtraces, set `RUST_LIB_BACKTRACE=1`;
29+
//! > - If you want only panics to have backtraces, set `RUST_BACKTRACE=1` and
30+
//! > `RUST_LIB_BACKTRACE=0`.
31+
32+
use crate::{FieldError, IntoFieldError, ScalarValue, Value};
33+
34+
impl<S: ScalarValue> IntoFieldError<S> for anyhow::Error {
35+
fn into_field_error(self) -> FieldError<S> {
36+
#[cfg(any(nightly, feature = "backtrace"))]
37+
let extensions = {
38+
let backtrace = self.backtrace().to_string();
39+
if backtrace == "disabled backtrace" {
40+
Value::Null
41+
} else {
42+
let mut obj = crate::value::Object::with_capacity(1);
43+
_ = obj.add_field(
44+
"backtrace",
45+
Value::List(
46+
backtrace
47+
.split('\n')
48+
.map(|line| Value::Scalar(line.to_owned().into()))
49+
.collect(),
50+
),
51+
);
52+
Value::Object(obj)
53+
}
54+
};
55+
#[cfg(not(any(nightly, feature = "backtrace")))]
56+
let extensions = Value::Null;
57+
58+
FieldError::new(self, extensions)
59+
}
60+
}
61+
62+
#[cfg(test)]
63+
mod test {
64+
use std::env;
65+
66+
use anyhow::anyhow;
67+
use serial_test::serial;
68+
69+
use crate::{
70+
execute, graphql_object, graphql_value, graphql_vars, parser::SourcePosition,
71+
EmptyMutation, EmptySubscription, RootNode,
72+
};
73+
74+
#[tokio::test]
75+
#[serial]
76+
async fn simple() {
77+
struct Root;
78+
79+
#[graphql_object]
80+
impl Root {
81+
fn err() -> anyhow::Result<i32> {
82+
Err(anyhow!("errored!"))
83+
}
84+
}
85+
86+
let prev_env = env::var("RUST_BACKTRACE").ok();
87+
env::set_var("RUST_BACKTRACE", "1");
88+
89+
const DOC: &str = r#"{
90+
err
91+
}"#;
92+
93+
let schema = RootNode::new(
94+
Root,
95+
EmptyMutation::<()>::new(),
96+
EmptySubscription::<()>::new(),
97+
);
98+
99+
let res = execute(DOC, None, &schema, &graphql_vars! {}, &()).await;
100+
101+
assert!(res.is_ok(), "failed: {:?}", res.unwrap_err());
102+
103+
let (val, errs) = res.unwrap();
104+
105+
assert_eq!(val, graphql_value!(null));
106+
assert_eq!(errs.len(), 1, "too many errors: {errs:?}");
107+
108+
let err = errs.first().unwrap();
109+
110+
assert_eq!(*err.location(), SourcePosition::new(14, 1, 12));
111+
assert_eq!(err.path(), &["err"]);
112+
113+
let err = err.error();
114+
115+
assert_eq!(err.message(), "errored!");
116+
#[cfg(not(any(nightly, feature = "backtrace")))]
117+
assert_eq!(err.extensions(), &graphql_value!(null));
118+
#[cfg(any(nightly, feature = "backtrace"))]
119+
assert_eq!(
120+
err.extensions()
121+
.as_object_value()
122+
.map(|ext| ext.contains_field("backtrace")),
123+
Some(true),
124+
"no `backtrace` in extensions: {err:?}",
125+
);
126+
127+
if let Some(val) = prev_env {
128+
env::set_var("RUST_BACKTRACE", val);
129+
}
130+
}
131+
}

juniper/src/integrations/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Provides GraphQLType implementations for some external types
22
3+
#[cfg(feature = "anyhow")]
4+
pub mod anyhow;
35
#[cfg(feature = "bigdecimal")]
46
pub mod bigdecimal;
57
#[cfg(feature = "bson")]

tests/integration/tests/codegen_object_attr.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ mod fallible_method {
332332

333333
impl<S: ScalarValue> IntoFieldError<S> for CustomError {
334334
fn into_field_error(self) -> FieldError<S> {
335-
juniper::FieldError::new("Whatever", graphql_value!({"code": "some"}))
335+
FieldError::new("Whatever", graphql_value!({"code": "some"}))
336336
}
337337
}
338338

0 commit comments

Comments
 (0)