Skip to main content

gqlrs_rocket/
rocket_impl.rs

1use core::any::Any;
2use std::io::Cursor;
3
4use async_graphql::{Executor, ParseRequestError, http::MultipartOptions};
5use rocket::{
6    data::{self, Data, FromData, ToByteUnit},
7    form::FromForm,
8    http::{ContentType, Header, Status},
9    response::{self, Responder},
10};
11use tokio_util::compat::TokioAsyncReadCompatExt;
12
13/// A batch request which can be extracted from a request's body.
14///
15/// # Examples
16///
17/// ```ignore
18/// #[rocket::post("/graphql", data = "<request>", format = "application/json", rank = 1)]
19/// async fn graphql_request(schema: State<'_, ExampleSchema>, request: BatchRequest) -> Response {
20///     request.execute(&schema).await
21/// }
22/// ```
23#[derive(Debug)]
24pub struct GraphQLBatchRequest(pub async_graphql::BatchRequest);
25
26impl GraphQLBatchRequest {
27    /// Shortcut method to execute the request on the executor.
28    pub async fn execute<E>(self, executor: &E) -> GraphQLResponse
29    where
30        E: Executor,
31    {
32        GraphQLResponse(executor.execute_batch(self.0).await)
33    }
34}
35
36#[rocket::async_trait]
37impl<'r> FromData<'r> for GraphQLBatchRequest {
38    type Error = ParseRequestError;
39
40    async fn from_data(req: &'r rocket::Request<'_>, data: Data<'r>) -> data::Outcome<'r, Self> {
41        let opts: MultipartOptions = req.rocket().state().copied().unwrap_or_default();
42
43        let request = async_graphql::http::receive_batch_body(
44            req.headers().get_one("Content-Type"),
45            data.open(
46                req.limits()
47                    .get("graphql")
48                    .unwrap_or_else(|| 128.kibibytes()),
49            )
50            .compat(),
51            opts,
52        )
53        .await;
54
55        match request {
56            Ok(request) => data::Outcome::Success(Self(request)),
57            Err(e) => data::Outcome::Error((
58                match e {
59                    ParseRequestError::PayloadTooLarge => Status::PayloadTooLarge,
60                    _ => Status::BadRequest,
61                },
62                e,
63            )),
64        }
65    }
66}
67
68/// A GraphQL request which can be extracted from the request's body.
69///
70/// # Examples
71///
72/// ```ignore
73/// #[rocket::post("/graphql", data = "<request>", format = "application/json", rank = 2)]
74/// async fn graphql_request(schema: State<'_, ExampleSchema>, request: Request) -> Result<Response, Status> {
75///     request.execute(&schema).await
76/// }
77/// ```
78#[derive(Debug)]
79pub struct GraphQLRequest(pub async_graphql::Request);
80
81impl GraphQLRequest {
82    /// Shortcut method to execute the request on the schema.
83    pub async fn execute<E>(self, executor: &E) -> GraphQLResponse
84    where
85        E: Executor,
86    {
87        GraphQLResponse(executor.execute(self.0).await.into())
88    }
89
90    /// Insert some data for this request.
91    #[must_use]
92    pub fn data<D: Any + Send + Sync>(mut self, data: D) -> Self {
93        self.0.data.insert(data);
94        self
95    }
96}
97
98impl From<GraphQLQuery> for GraphQLRequest {
99    fn from(query: GraphQLQuery) -> Self {
100        let mut request = async_graphql::Request::new(query.query);
101
102        if let Some(operation_name) = query.operation_name {
103            request = request.operation_name(operation_name);
104        }
105
106        if let Some(variables) = query.variables {
107            let value = serde_json::from_str(&variables).unwrap_or_default();
108            let variables = async_graphql::Variables::from_json(value);
109            request = request.variables(variables);
110        }
111
112        GraphQLRequest(request)
113    }
114}
115
116/// A GraphQL request which can be extracted from a query string.
117///
118/// # Examples
119///
120/// ```ignore
121/// #[rocket::get("/graphql?<query..>")]
122/// async fn graphql_query(schema: State<'_, ExampleSchema>, query: Query) -> Result<Response, Status> {
123///     query.execute(&schema).await
124/// }
125/// ```
126#[derive(FromForm, Debug)]
127pub struct GraphQLQuery {
128    query: String,
129    #[field(name = "operationName")]
130    operation_name: Option<String>,
131    variables: Option<String>,
132}
133
134impl GraphQLQuery {
135    /// Shortcut method to execute the request on the schema.
136    pub async fn execute<E>(self, executor: &E) -> GraphQLResponse
137    where
138        E: Executor,
139    {
140        let request: GraphQLRequest = self.into();
141        request.execute(executor).await
142    }
143}
144
145#[rocket::async_trait]
146impl<'r> FromData<'r> for GraphQLRequest {
147    type Error = ParseRequestError;
148
149    async fn from_data(req: &'r rocket::Request<'_>, data: Data<'r>) -> data::Outcome<'r, Self> {
150        GraphQLBatchRequest::from_data(req, data)
151            .await
152            .and_then(|request| match request.0.into_single() {
153                Ok(single) => data::Outcome::Success(Self(single)),
154                Err(e) => data::Outcome::Error((Status::BadRequest, e)),
155            })
156    }
157}
158
159/// Wrapper around `async-graphql::Response` that is a Rocket responder so it
160/// can be returned from a routing function in Rocket.
161///
162/// It contains a `BatchResponse` but since a response is a type of batch
163/// response it works for both.
164#[derive(Debug)]
165pub struct GraphQLResponse(pub async_graphql::BatchResponse);
166
167impl From<async_graphql::BatchResponse> for GraphQLResponse {
168    fn from(batch: async_graphql::BatchResponse) -> Self {
169        Self(batch)
170    }
171}
172impl From<async_graphql::Response> for GraphQLResponse {
173    fn from(res: async_graphql::Response) -> Self {
174        Self(res.into())
175    }
176}
177
178impl<'r> Responder<'r, 'static> for GraphQLResponse {
179    fn respond_to(self, _: &'r rocket::Request<'_>) -> response::Result<'static> {
180        let body = serde_json::to_string(&self.0).unwrap();
181
182        let mut response = rocket::Response::new();
183        response.set_header(ContentType::new("application", "json"));
184
185        if self.0.is_ok()
186            && let Some(cache_control) = self.0.cache_control().value()
187        {
188            response.set_header(Header::new("cache-control", cache_control));
189        }
190
191        for (name, value) in self.0.http_headers_iter() {
192            if let Ok(value) = value.to_str() {
193                response.adjoin_header(Header::new(name.as_str().to_string(), value.to_string()));
194            }
195        }
196
197        response.set_sized_body(body.len(), Cursor::new(body));
198
199        Ok(response)
200    }
201}