Skip to main content

async_graphql_test/
executor.rs

1use std::future::Future;
2
3use async_graphql::PathSegment;
4use itertools::Itertools;
5
6use crate::matchers::MatcherError;
7use crate::script::Script;
8
9/// Execute a sequence of GraphQL requests, calling the provided handler on each
10/// request.
11///
12/// The handler can be as simple as `|req| schema.execute(req)`, but you can
13/// also put additional logic there.
14///
15/// Queries and mutations starting with `_` are not returned in the final list
16/// of responses. This is useful for ommiting the feedback from the initial
17/// "populate the DB" boilerplate in the response snapshot.
18pub async fn execute_script<F, Fut>(
19    mut execute_request: F,
20    script: Script,
21) -> Vec<async_graphql::Response>
22where
23    F: FnMut(async_graphql::Request) -> Fut,
24    Fut: Future<Output = async_graphql::Response>,
25{
26    let requests = script.requests;
27    let mut acc = Vec::with_capacity(requests.len());
28
29    if requests.is_empty() {
30        panic!("at least one query or mutation must be present");
31    }
32
33    for mut req in requests {
34        // reset operation_name, otherwise an error is triggered on execution
35        // https://github.com/async-graphql/async-graphql/blob/df11cfe35fa0762dadfd86a2cbe1f084d3061469/src/schema.rs#L839
36        let op = req
37            .parsed_query()
38            .unwrap()
39            .operations
40            .iter()
41            .next()
42            .unwrap()
43            .1
44            .clone();
45        let op_type = op.node.ty;
46
47        let op_name = req.operation_name.take().unwrap_or_default();
48        let failure_causes_panic = op_name.starts_with('_');
49
50        let res = execute_request(req).await;
51
52        let has_matcher_err = res
53            .errors
54            .iter()
55            .any(|err| err.source::<MatcherError>().is_some());
56
57        if res.is_err() && (has_matcher_err || failure_causes_panic) {
58            // we'll print only the first error and exit
59            if let Some(err) = res.errors.first() {
60                // err formating and the context around must be moved to the MatcherError
61                let path = GraphQLPathSegments(&err.path);
62                let source = err.source.as_deref();
63                // printing the error like this is more readable than printing it in the `panic!`
64                eprintln!(
65                    "{:?} '{}' ({}) raised an error at line {}\n\n{}\n\nSource:\n{:?}\n",
66                    op_type, op_name, path, err.locations[0], err.message, source,
67                );
68                // abort execution after we've printed the error
69                panic!();
70            }
71        }
72
73        if !failure_causes_panic || acc.len() == 1 {
74            acc.push(res);
75        }
76    }
77
78    acc
79}
80
81struct GraphQLPathSegments<'a>(&'a [PathSegment]);
82
83impl std::fmt::Display for GraphQLPathSegments<'_> {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        self.0
86            .iter()
87            .format_with(".", |segment, f| match segment {
88                PathSegment::Field(name) => f(name),
89                PathSegment::Index(idx) => f(&format_args!("[{idx}]")),
90            })
91            .fmt(f)
92    }
93}