1use actix_web::{
13 FromRequest, HttpRequest, HttpResponse, Responder,
14 body::BoxBody,
15 error::{InternalError, JsonPayloadError},
16 http::{StatusCode, header},
17 web::Html,
18};
19use necrassrs::{JsonMap, Request, Response};
20use serde::Deserialize;
21
22pub fn graphiql_html(endpoint_url: &str) -> Html {
28 let endpoint_url = endpoint_url
29 .replace('&', "&")
30 .replace('"', """)
31 .replace('\'', "'")
32 .replace('<', "<")
33 .replace('>', ">");
34 Html::new(include_str!("graphiql.html").replace("__NECRASSRS_ENDPOINT__", &endpoint_url))
35}
36
37pub struct GraphQLRequest(
45 pub Request,
47);
48
49#[derive(Deserialize)]
50#[serde(rename_all = "camelCase")]
51struct RequestBody {
52 query: String,
53 variables: Option<JsonMap>,
54 operation_name: Option<String>,
55}
56
57impl FromRequest for GraphQLRequest {
58 type Error = actix_web::Error;
59 type Future = std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self, Self::Error>>>>;
60
61 fn from_request(
62 req: &actix_web::HttpRequest,
63 payload: &mut actix_web::dev::Payload,
64 ) -> Self::Future {
65 let negotiation = negotiated_media_type(req);
66 let extraction = actix_web::web::Json::<RequestBody>::from_request(req, payload);
67
68 Box::pin(async move {
69 negotiation?;
70 let body = extraction
71 .await
72 .map_err(|error| {
73 let status = match error.as_error::<JsonPayloadError>() {
74 Some(JsonPayloadError::ContentType) => StatusCode::UNSUPPORTED_MEDIA_TYPE,
75 Some(JsonPayloadError::Deserialize(cause)) if cause.is_data() => {
76 StatusCode::UNPROCESSABLE_ENTITY
77 }
78 _ => return error,
79 };
80 let response = HttpResponse::build(status)
81 .content_type("text/plain; charset=utf-8")
82 .body(error.to_string());
83 InternalError::from_response(error, response).into()
84 })?
85 .into_inner();
86 let mut request = Request::new(body.query);
87
88 if let Some(variables) = body.variables {
89 request = request.with_variables(variables);
90 }
91 if let Some(operation_name) = body.operation_name {
92 request = request.with_operation_name(operation_name);
93 }
94
95 Ok(Self(request))
96 })
97 }
98}
99
100pub struct GraphQLResponse(
106 pub Response,
108);
109
110impl Responder for GraphQLResponse {
111 type Body = BoxBody;
112
113 fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
114 let media_type = match negotiated_media_type(req) {
115 Ok(media_type) => media_type,
116 Err(error) => return error.error_response(),
117 };
118 let status = if self.0.is_syntax_error() {
119 StatusCode::BAD_REQUEST
120 } else if self.0.is_request_error() {
121 StatusCode::UNPROCESSABLE_ENTITY
122 } else {
123 StatusCode::OK
124 };
125 let media_type = if status.is_success() {
126 media_type
127 } else {
128 necrassrs_http::GRAPHQL_JSON
129 };
130 HttpResponse::build(status)
131 .insert_header((header::VARY, "Accept"))
132 .content_type(media_type)
133 .json(self.0)
134 }
135}
136
137fn negotiated_media_type(req: &HttpRequest) -> Result<&'static str, actix_web::Error> {
138 let headers = req
139 .headers()
140 .get_all(header::ACCEPT)
141 .map(|value| value.to_str())
142 .collect::<Result<Vec<_>, _>>();
143 headers
144 .ok()
145 .and_then(necrassrs_http::response_media_type)
146 .ok_or_else(|| {
147 InternalError::from_response(
148 "No acceptable GraphQL response media type",
149 HttpResponse::NotAcceptable()
150 .insert_header((header::VARY, "Accept"))
151 .finish(),
152 )
153 .into()
154 })
155}