use axum::{
response::Html,
routing::get,
Router,
};
#[derive(Debug, Clone)]
pub struct GraphQLConfig {
pub path: String,
}
impl Default for GraphQLConfig {
fn default() -> Self {
Self {
path: "/graphql".to_string(),
}
}
}
impl GraphQLConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_path(mut self, path: impl Into<String>) -> Self {
self.path = path.into();
self
}
}
pub fn graphql_router_dynamic(
schema: async_graphql::dynamic::Schema,
config: GraphQLConfig,
) -> Router {
async fn graphql_handler(
axum::extract::State(schema): axum::extract::State<async_graphql::dynamic::Schema>,
request: async_graphql_axum::GraphQLRequest,
) -> async_graphql_axum::GraphQLResponse {
schema.execute(request.into_inner()).await.into()
}
Router::new()
.route(&config.path, axum::routing::post(graphql_handler))
.with_state(schema)
}
pub fn graphql_router<Q, M, S>(
schema: async_graphql::Schema<Q, M, S>,
config: GraphQLConfig,
) -> Router
where
Q: async_graphql::ObjectType + 'static,
M: async_graphql::ObjectType + 'static,
S: async_graphql::SubscriptionType + 'static,
{
async fn graphql_handler<Q, M, S>(
axum::extract::State(schema): axum::extract::State<async_graphql::Schema<Q, M, S>>,
request: async_graphql_axum::GraphQLRequest,
) -> async_graphql_axum::GraphQLResponse
where
Q: async_graphql::ObjectType + 'static,
M: async_graphql::ObjectType + 'static,
S: async_graphql::SubscriptionType + 'static,
{
schema.execute(request.into_inner()).await.into()
}
Router::new()
.route(&config.path, axum::routing::post(graphql_handler::<Q, M, S>))
.with_state(schema)
}
fn graphiql_html(endpoint: &str) -> String {
format!(
r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>GraphiQL — sz-rust</title>
<style>
body {{ height: 100%; margin: 0; overflow: hidden; }}
#graphiql {{ height: 100vh; }}
</style>
<script src="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/graphiql@3.0.10/graphiql.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphiql@3.0.10/graphiql.min.css"/>
</head>
<body>
<div id="graphiql"></div>
<script>
ReactDOM.render(
React.createElement(GraphiQL, {{ fetcher: GraphiQL.createFetcher({{ url: "{endpoint}" }}) }}),
document.getElementById('graphiql'),
);
</script>
</body>
</html>"#,
endpoint = endpoint
)
}
pub fn graphiql_route(graphql_endpoint: &str) -> Router {
let html = graphiql_html(graphql_endpoint);
Router::new().route(
"/graphiql",
get(move || async move { Html(html.clone()) }),
)
}
pub fn graphql_with_graphiql_dynamic(
schema: async_graphql::dynamic::Schema,
config: GraphQLConfig,
) -> Router {
let endpoint = config.path.clone();
graphql_router_dynamic(schema, config).merge(graphiql_route(&endpoint))
}
pub fn graphql_with_graphiql<Q, M, S>(
schema: async_graphql::Schema<Q, M, S>,
config: GraphQLConfig,
) -> Router
where
Q: async_graphql::ObjectType + 'static,
M: async_graphql::ObjectType + 'static,
S: async_graphql::SubscriptionType + 'static,
{
let endpoint = config.path.clone();
graphql_router(schema, config).merge(graphiql_route(&endpoint))
}
pub type GraphQLRequest = async_graphql_axum::GraphQLRequest;
pub type GraphQLResponse = async_graphql_axum::GraphQLResponse;
#[cfg(test)]
mod tests {
use super::*;
use tower::ServiceExt;
#[test]
fn test_graphql_config_default() {
let config = GraphQLConfig::default();
assert_eq!(config.path, "/graphql");
}
#[test]
fn test_graphql_config_builder() {
let config = GraphQLConfig::new().with_path("/api/graphql");
assert_eq!(config.path, "/api/graphql");
}
fn test_dynamic_schema() -> async_graphql::dynamic::Schema {
use async_graphql::dynamic::{Field, FieldFuture, Object, Schema, TypeRef};
use async_graphql::Value;
let query = Object::new("Query").field(Field::new(
"hello",
TypeRef::named("String"),
|_| FieldFuture::from_value(Some(Value::from("world"))),
));
Schema::build("Query", None, None)
.register(query)
.finish()
.unwrap()
}
#[tokio::test]
async fn test_graphql_router_dynamic_hello_query() {
use axum::body::Body;
use http_body_util::BodyExt;
let schema = test_dynamic_schema();
let app = graphql_router_dynamic(schema, GraphQLConfig::default());
let response = app
.oneshot(
axum::http::Request::builder()
.method("POST")
.uri("/graphql")
.header("content-type", "application/json")
.body(Body::from(
r#"{"query":"{ hello }"}"#.to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["data"]["hello"], "world");
}
#[tokio::test]
async fn test_graphql_router_dynamic_custom_path() {
use axum::body::Body;
use http_body_util::BodyExt;
let schema = test_dynamic_schema();
let app = graphql_router_dynamic(schema, GraphQLConfig::new().with_path("/api/gql"));
let response = app
.oneshot(
axum::http::Request::builder()
.method("POST")
.uri("/api/gql")
.header("content-type", "application/json")
.body(Body::from(
r#"{"query":"{ hello }"}"#.to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::OK);
}
#[tokio::test]
async fn test_graphql_router_dynamic_invalid_query() {
use axum::body::Body;
use http_body_util::BodyExt;
let schema = test_dynamic_schema();
let app = graphql_router_dynamic(schema, GraphQLConfig::default());
let response = app
.oneshot(
axum::http::Request::builder()
.method("POST")
.uri("/graphql")
.header("content-type", "application/json")
.body(Body::from(
r#"{"query":"{ nonexistentField }"}"#.to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json["errors"].is_array());
assert!(!json["errors"].as_array().unwrap().is_empty());
}
#[tokio::test]
async fn test_graphiql_route_returns_html() {
use axum::body::Body;
use http_body_util::BodyExt;
let app = graphiql_route("/graphql");
let response = app
.oneshot(
axum::http::Request::builder()
.method("GET")
.uri("/graphiql")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let html = String::from_utf8(body.to_vec()).unwrap();
assert!(html.contains("GraphiQL"));
assert!(html.contains("/graphql"));
}
#[tokio::test]
async fn test_graphql_with_graphiql_dynamic() {
use axum::body::Body;
use http_body_util::BodyExt;
let schema = test_dynamic_schema();
let app = graphql_with_graphiql_dynamic(schema, GraphQLConfig::default());
let response = app
.clone()
.oneshot(
axum::http::Request::builder()
.method("POST")
.uri("/graphql")
.header("content-type", "application/json")
.body(Body::from(
r#"{"query":"{ hello }"}"#.to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::OK);
let response = app
.oneshot(
axum::http::Request::builder()
.method("GET")
.uri("/graphiql")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), axum::http::StatusCode::OK);
let body = response.into_body().collect().await.unwrap().to_bytes();
let html = String::from_utf8(body.to_vec()).unwrap();
assert!(html.contains("GraphiQL"));
}
#[test]
fn test_graphiql_html_contains_endpoint() {
let html = graphiql_html("/my-graphql");
assert!(html.contains("/my-graphql"));
assert!(html.contains("GraphiQL"));
assert!(html.contains("<!DOCTYPE html>"));
}
#[test]
fn test_graphiql_html_contains_cdn_links() {
let html = graphiql_html("/graphql");
assert!(html.contains("react@18"));
assert!(html.contains("graphiql@3"));
}
}