From 26dfea700137cce1ba5944c47a6a9f024f7d84dd Mon Sep 17 00:00:00 2001 From: tyranron Date: Wed, 22 Oct 2025 20:31:14 +0300 Subject: [PATCH 1/7] Bootstrap `GraphQLRequest::extensions` field --- juniper/src/http/mod.rs | 228 ++++++++++++++++++++-------------------- 1 file changed, 116 insertions(+), 112 deletions(-) diff --git a/juniper/src/http/mod.rs b/juniper/src/http/mod.rs index 71f97d581..5d00a41c4 100644 --- a/juniper/src/http/mod.rs +++ b/juniper/src/http/mod.rs @@ -24,7 +24,7 @@ use crate::{ /// For GET, you will need to parse the query string and extract "query", /// "operationName", and "variables" manually. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -pub struct GraphQLRequest +pub struct GraphQLRequest> where S: ScalarValue, { @@ -42,6 +42,10 @@ where serialize = "InputValue: Serialize", ))] pub variables: Option>, + + /// Optional implementation-specific additional information. + #[serde(default)] + pub extensions: Ext, } impl GraphQLRequest @@ -76,6 +80,7 @@ where query, operation_name, variables, + extensions: Default::default(), } } @@ -129,6 +134,113 @@ where } } +/// Simple wrapper around GraphQLRequest to allow the handling of Batch requests. +#[derive(Debug, Deserialize, PartialEq)] +#[serde(untagged)] +#[serde(bound = "GraphQLRequest: Deserialize<'de>")] +pub enum GraphQLBatchRequest> +where + S: ScalarValue, +{ + /// A single operation request. + Single(GraphQLRequest), + + /// A batch operation request. + /// + /// Empty batch is considered as invalid value, so cannot be deserialized. + #[serde(deserialize_with = "deserialize_non_empty_batch")] + Batch(Vec>), +} + +fn deserialize_non_empty_batch<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: de::Deserializer<'de>, + T: Deserialize<'de>, +{ + use de::Error as _; + + let v = Vec::::deserialize(deserializer)?; + if v.is_empty() { + Err(D::Error::invalid_length( + 0, + &"non-empty batch of GraphQL requests", + )) + } else { + Ok(v) + } +} + +impl GraphQLBatchRequest +where + S: ScalarValue, +{ + /// Execute a GraphQL batch request synchronously using the specified schema and context + /// + /// This is a simple wrapper around the `execute_sync` function exposed in GraphQLRequest. + pub fn execute_sync<'a, QueryT, MutationT, SubscriptionT>( + &'a self, + root_node: &'a RootNode, + context: &QueryT::Context, + ) -> GraphQLBatchResponse + where + QueryT: GraphQLType, + MutationT: GraphQLType, + SubscriptionT: GraphQLType, + { + match self { + Self::Single(req) => GraphQLBatchResponse::Single(req.execute_sync(root_node, context)), + Self::Batch(reqs) => GraphQLBatchResponse::Batch( + reqs.iter() + .map(|req| req.execute_sync(root_node, context)) + .collect(), + ), + } + } + + /// Executes a GraphQL request using the specified schema and context + /// + /// This is a simple wrapper around the `execute` function exposed in + /// GraphQLRequest + pub async fn execute<'a, QueryT, MutationT, SubscriptionT>( + &'a self, + root_node: &'a RootNode, + context: &'a QueryT::Context, + ) -> GraphQLBatchResponse + where + QueryT: GraphQLTypeAsync, + QueryT::TypeInfo: Sync, + QueryT::Context: Sync, + MutationT: GraphQLTypeAsync, + MutationT::TypeInfo: Sync, + SubscriptionT: GraphQLSubscriptionType, + SubscriptionT::TypeInfo: Sync, + S: Send + Sync, + { + match self { + Self::Single(req) => { + let resp = req.execute(root_node, context).await; + GraphQLBatchResponse::Single(resp) + } + Self::Batch(reqs) => { + let resps = futures::future::join_all( + reqs.iter().map(|req| req.execute(root_node, context)), + ) + .await; + GraphQLBatchResponse::Batch(resps) + } + } + } + + /// The operation names of the request. + pub fn operation_names(&self) -> Vec> { + match self { + Self::Single(req) => vec![req.operation_name.as_deref()], + Self::Batch(reqs) => reqs.iter().map(|r| r.operation_name.as_deref()).collect(), + } + } +} + + /// Resolve a GraphQL subscription into `Value` using the /// specified schema and context. /// This is a wrapper around the `resolve_into_stream` function exposed at the top @@ -208,8 +320,8 @@ where where S: ser::Serializer, { - match self.0 { - Ok((ref res, ref err)) => { + match &self.0 { + Ok((res, err)) => { let mut map = serializer.serialize_map(None)?; map.serialize_key("data")?; @@ -222,7 +334,7 @@ where map.end() } - Err(ref err) => { + Err(err) => { let mut map = serializer.serialize_map(Some(1))?; map.serialize_key("errors")?; map.serialize_value(err)?; @@ -232,114 +344,6 @@ where } } -/// Simple wrapper around GraphQLRequest to allow the handling of Batch requests. -#[derive(Debug, Deserialize, PartialEq)] -#[serde(untagged)] -#[serde(bound = "InputValue: Deserialize<'de>")] -pub enum GraphQLBatchRequest -where - S: ScalarValue, -{ - /// A single operation request. - Single(GraphQLRequest), - - /// A batch operation request. - /// - /// Empty batch is considered as invalid value, so cannot be deserialized. - #[serde(deserialize_with = "deserialize_non_empty_batch")] - Batch(Vec>), -} - -fn deserialize_non_empty_batch<'de, D, T>(deserializer: D) -> Result, D::Error> -where - D: de::Deserializer<'de>, - T: Deserialize<'de>, -{ - use de::Error as _; - - let v = Vec::::deserialize(deserializer)?; - if v.is_empty() { - Err(D::Error::invalid_length( - 0, - &"non-empty batch of GraphQL requests", - )) - } else { - Ok(v) - } -} - -impl GraphQLBatchRequest -where - S: ScalarValue, -{ - /// Execute a GraphQL batch request synchronously using the specified schema and context - /// - /// This is a simple wrapper around the `execute_sync` function exposed in GraphQLRequest. - pub fn execute_sync<'a, QueryT, MutationT, SubscriptionT>( - &'a self, - root_node: &'a RootNode, - context: &QueryT::Context, - ) -> GraphQLBatchResponse - where - QueryT: GraphQLType, - MutationT: GraphQLType, - SubscriptionT: GraphQLType, - { - match *self { - Self::Single(ref req) => { - GraphQLBatchResponse::Single(req.execute_sync(root_node, context)) - } - Self::Batch(ref reqs) => GraphQLBatchResponse::Batch( - reqs.iter() - .map(|req| req.execute_sync(root_node, context)) - .collect(), - ), - } - } - - /// Executes a GraphQL request using the specified schema and context - /// - /// This is a simple wrapper around the `execute` function exposed in - /// GraphQLRequest - pub async fn execute<'a, QueryT, MutationT, SubscriptionT>( - &'a self, - root_node: &'a RootNode, - context: &'a QueryT::Context, - ) -> GraphQLBatchResponse - where - QueryT: GraphQLTypeAsync, - QueryT::TypeInfo: Sync, - QueryT::Context: Sync, - MutationT: GraphQLTypeAsync, - MutationT::TypeInfo: Sync, - SubscriptionT: GraphQLSubscriptionType, - SubscriptionT::TypeInfo: Sync, - S: Send + Sync, - { - match self { - Self::Single(req) => { - let resp = req.execute(root_node, context).await; - GraphQLBatchResponse::Single(resp) - } - Self::Batch(reqs) => { - let resps = futures::future::join_all( - reqs.iter().map(|req| req.execute(root_node, context)), - ) - .await; - GraphQLBatchResponse::Batch(resps) - } - } - } - - /// The operation names of the request. - pub fn operation_names(&self) -> Vec> { - match self { - Self::Single(req) => vec![req.operation_name.as_deref()], - Self::Batch(reqs) => reqs.iter().map(|r| r.operation_name.as_deref()).collect(), - } - } -} - /// Simple wrapper around the result (GraphQLResponse) from executing a GraphQLBatchRequest /// /// This struct implements Serialize, so you can simply serialize this From 6c54a0b7ebef8eb29de6510f737712de498d69ec Mon Sep 17 00:00:00 2001 From: tyranron Date: Wed, 22 Oct 2025 20:46:55 +0300 Subject: [PATCH 2/7] Some improvements [skip ci] --- juniper/CHANGELOG.md | 4 ++++ juniper/src/http/mod.rs | 12 +++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index b7a19c4e3..0fd8b924d 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -18,6 +18,8 @@ All user visible changes to `juniper` crate will be documented in this file. Thi - Added `description` field to `ast::Operation`, `ast::Fragment` and `ast::VariableDefinition`. ([#1349], [graphql/graphql-spec#1170]) - Renamed `ast::VariableDefinitions` to `ast::VariablesDefinition`: ([#1353], [graphql/graphql-spec#916]) - Renamed `ast::Operation::variable_definitions` field to `variables_definition`. + - Added `extenstions` field to `http::GraphQLRequest`. ([#1356], [graphql/graphql-spec#976]) + - Added `Ext` type parameter to `http::GraphQLRequest` and `http::GraphQLBatchRequest` defaulting to `Variables`. ([#1356], [graphql/graphql-spec#976]) - Changed `ScalarToken::String` to contain raw quoted and escaped `StringLiteral` (was unquoted but escaped string before). ([#1349]) - Added `LexerError::UnterminatedBlockString` variant. ([#1349]) @@ -72,12 +74,14 @@ All user visible changes to `juniper` crate will be documented in this file. Thi [#1353]: /../../pull/1353 [#1354]: /../../pull/1354 [#1355]: /../../pull/1355 +[#1356]: /../../pull/1356 [graphql/graphql-spec#525]: https://github.com/graphql/graphql-spec/pull/525 [graphql/graphql-spec#687]: https://github.com/graphql/graphql-spec/issues/687 [graphql/graphql-spec#805]: https://github.com/graphql/graphql-spec/pull/805 [graphql/graphql-spec#825]: https://github.com/graphql/graphql-spec/pull/825 [graphql/graphql-spec#849]: https://github.com/graphql/graphql-spec/pull/849 [graphql/graphql-spec#916]: https://github.com/graphql/graphql-spec/pull/916 +[graphql/graphql-spec#976]: https://github.com/graphql/graphql-spec/pull/976 [graphql/graphql-spec#1040]: https://github.com/graphql/graphql-spec/pull/1040 [graphql/graphql-spec#1142]: https://github.com/graphql/graphql-spec/pull/1142 [graphql/graphql-spec#1170]: https://github.com/graphql/graphql-spec/pull/1170 diff --git a/juniper/src/http/mod.rs b/juniper/src/http/mod.rs index 5d00a41c4..aba4eac66 100644 --- a/juniper/src/http/mod.rs +++ b/juniper/src/http/mod.rs @@ -48,7 +48,7 @@ where pub extensions: Ext, } -impl GraphQLRequest +impl GraphQLRequest where S: ScalarValue, { @@ -75,12 +75,15 @@ where query: String, operation_name: Option, variables: Option>, - ) -> Self { + ) -> Self + where + Ext: Default, + { Self { query, operation_name, variables, - extensions: Default::default(), + extensions: Ext::default(), } } @@ -170,7 +173,7 @@ where } } -impl GraphQLBatchRequest +impl GraphQLBatchRequest where S: ScalarValue, { @@ -240,7 +243,6 @@ where } } - /// Resolve a GraphQL subscription into `Value` using the /// specified schema and context. /// This is a wrapper around the `resolve_into_stream` function exposed at the top From 33b9f9090f9df216c224088880cf37dea1d09ffd Mon Sep 17 00:00:00 2001 From: tyranron Date: Wed, 22 Oct 2025 21:24:58 +0300 Subject: [PATCH 3/7] Remove obsolete stuff [skip ci] --- juniper/CHANGELOG.md | 3 ++ juniper/src/http/mod.rs | 34 +++--------- juniper/src/tests/subscriptions.rs | 7 ++- juniper_actix/src/lib.rs | 16 ++++-- juniper_axum/src/extract.rs | 83 +++++++++++++++++------------- juniper_hyper/src/lib.rs | 16 ++++-- juniper_rocket/src/lib.rs | 41 +++++++++++---- juniper_warp/src/lib.rs | 20 ++++--- 8 files changed, 131 insertions(+), 89 deletions(-) diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 0fd8b924d..31f127451 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -22,6 +22,9 @@ All user visible changes to `juniper` crate will be documented in this file. Thi - Added `Ext` type parameter to `http::GraphQLRequest` and `http::GraphQLBatchRequest` defaulting to `Variables`. ([#1356], [graphql/graphql-spec#976]) - Changed `ScalarToken::String` to contain raw quoted and escaped `StringLiteral` (was unquoted but escaped string before). ([#1349]) - Added `LexerError::UnterminatedBlockString` variant. ([#1349]) +- `http::GraphQLRequest`: + - Removed `new()` constructor in favor of constructing it directly. ([#1356]) + - Removed deprecated `operation_name()` method in favor of direct `operation_name` field. ([#1356]) ### Added diff --git a/juniper/src/http/mod.rs b/juniper/src/http/mod.rs index aba4eac66..82ff17f69 100644 --- a/juniper/src/http/mod.rs +++ b/juniper/src/http/mod.rs @@ -24,6 +24,8 @@ use crate::{ /// For GET, you will need to parse the query string and extract "query", /// "operationName", and "variables" manually. #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +// Should be specified as top-level, otherwise `serde` infers incorrect `Ext: Default` bound. +#[serde(bound(deserialize = "Ext: Deserialize<'de>"))] pub struct GraphQLRequest> where S: ScalarValue, @@ -43,22 +45,17 @@ where ))] pub variables: Option>, - /// Optional implementation-specific additional information. - #[serde(default)] - pub extensions: Ext, + /// Optional implementation-specific additional information (as per [spec]). + /// + /// [spec]: https://spec.graphql.org/September2025#sel-FANHLBBgBBvC0vW + #[serde(default, skip_serializing_if = "Option::is_none")] + pub extensions: Option, } impl GraphQLRequest where S: ScalarValue, { - // TODO: Remove in 0.17 `juniper` version. - /// Returns the `operation_name` associated with this request. - #[deprecated(since = "0.16.0", note = "Use the direct field access instead.")] - pub fn operation_name(&self) -> Option<&str> { - self.operation_name.as_deref() - } - /// Returns operation [`Variables`] defined withing this request. pub fn variables(&self) -> Variables { self.variables @@ -70,23 +67,6 @@ where .unwrap_or_default() } - /// Construct a new GraphQL request from parts - pub fn new( - query: String, - operation_name: Option, - variables: Option>, - ) -> Self - where - Ext: Default, - { - Self { - query, - operation_name, - variables, - extensions: Ext::default(), - } - } - /// Execute a GraphQL request synchronously using the specified schema and context /// /// This is a simple wrapper around the `execute_sync` function exposed at the diff --git a/juniper/src/tests/subscriptions.rs b/juniper/src/tests/subscriptions.rs index 80a23b160..0437f6e94 100644 --- a/juniper/src/tests/subscriptions.rs +++ b/juniper/src/tests/subscriptions.rs @@ -94,7 +94,12 @@ fn create_and_execute( ), Vec>, > { - let request = GraphQLRequest::new(query, None, None); + let request = GraphQLRequest { + query, + operation_name: None, + variables: None, + extensions: None, + }; let root_node = Schema::new(MyQuery, EmptyMutation::new(), MySubscription); diff --git a/juniper_actix/src/lib.rs b/juniper_actix/src/lib.rs index 52527b80d..17580901b 100644 --- a/juniper_actix/src/lib.rs +++ b/juniper_actix/src/lib.rs @@ -36,7 +36,12 @@ where variables, } = get_req; let variables = variables.map(|s| serde_json::from_str(&s).unwrap()); - Self::new(query, operation_name, variables) + Self { + query, + operation_name, + variables, + extensions: None, + } } } @@ -117,9 +122,12 @@ where } "application/graphql" => { let body = String::from_request(&req, &mut payload.into_inner()).await?; - Ok(GraphQLBatchRequest::Single(GraphQLRequest::new( - body, None, None, - ))) + Ok(GraphQLBatchRequest::Single(GraphQLRequest { + query: body, + operation_name: None, + variables: None, + extensions: None, + })) } _ => Err(JsonPayloadError::ContentType), }?; diff --git a/juniper_axum/src/extract.rs b/juniper_axum/src/extract.rs index 6ebf0af54..3780c4777 100644 --- a/juniper_axum/src/extract.rs +++ b/juniper_axum/src/extract.rs @@ -130,9 +130,12 @@ where String::from_request(req, state) .await .map(|body| { - Self(GraphQLBatchRequest::Single(GraphQLRequest::new( - body, None, None, - ))) + Self(GraphQLBatchRequest::Single(GraphQLRequest { + query: body, + operation_name: None, + variables: None, + extensions: None, + })) }) .map_err(|_| (StatusCode::BAD_REQUEST, "Not valid UTF-8 body").into_response()) } @@ -170,11 +173,12 @@ impl TryFrom for GraphQLRequest { operation_name, variables, } = req; - Ok(Self::new( + Ok(Self { query, operation_name, - variables.map(|v| serde_json::from_str(&v)).transpose()?, - )) + variables: variables.map(|v| serde_json::from_str(&v)).transpose()?, + extensions: None, + }) } } @@ -198,11 +202,12 @@ mod juniper_request_tests { .body(Body::empty()) .unwrap_or_else(|e| panic!("cannot build `Request`: {e}")); - let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest::new( - "{ add(a: 2, b: 3) }".into(), - None, - None, - ))); + let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest { + query: "{ add(a: 2, b: 3) }".into(), + operation_name: None, + variables: None, + extensions: None, + })); assert_eq!(do_from_request(req).await, expected); } @@ -219,11 +224,13 @@ mod juniper_request_tests { .body(Body::empty()) .unwrap_or_else(|e| panic!("cannot build `Request`: {e}")); - let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest::new( - "query($id: String!) { human(id: $id) { id, name, appearsIn, homePlanet } }".into(), - None, - Some(graphql_input_value!({"id": "1000"})), - ))); + let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest { + query: "query($id: String!) { human(id: $id) { id, name, appearsIn, homePlanet } }" + .into(), + operation_name: None, + variables: None, + extensions: None, + })); assert_eq!(do_from_request(req).await, expected); } @@ -235,11 +242,12 @@ mod juniper_request_tests { .body(Body::from(r#"{"query": "{ add(a: 2, b: 3) }"}"#)) .unwrap_or_else(|e| panic!("cannot build `Request`: {e}")); - let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest::new( - "{ add(a: 2, b: 3) }".to_string(), - None, - None, - ))); + let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest { + query: "{ add(a: 2, b: 3) }".to_string(), + operation_name: None, + variables: None, + extensions: None, + })); assert_eq!(do_from_request(req).await, expected); } @@ -251,11 +259,12 @@ mod juniper_request_tests { .body(Body::from(r#"{"query": "{ add(a: 2, b: 3) }"}"#)) .unwrap_or_else(|e| panic!("cannot build `Request`: {e}")); - let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest::new( - "{ add(a: 2, b: 3) }".to_string(), - None, - None, - ))); + let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest { + query: "{ add(a: 2, b: 3) }".into(), + operation_name: None, + variables: None, + extensions: None, + })); assert_eq!(do_from_request(req).await, expected); } @@ -267,11 +276,12 @@ mod juniper_request_tests { .body(Body::from(r#"{ add(a: 2, b: 3) }"#)) .unwrap_or_else(|e| panic!("cannot build `Request`: {e}")); - let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest::new( - "{ add(a: 2, b: 3) }".to_string(), - None, - None, - ))); + let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest { + query: "{ add(a: 2, b: 3) }".to_string(), + operation_name: None, + variables: None, + extensions: None, + })); assert_eq!(do_from_request(req).await, expected); } @@ -283,11 +293,12 @@ mod juniper_request_tests { .body(Body::from(r#"{ add(a: 2, b: 3) }"#)) .unwrap_or_else(|e| panic!("cannot build `Request`: {e}")); - let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest::new( - "{ add(a: 2, b: 3) }".to_string(), - None, - None, - ))); + let expected = JuniperRequest(GraphQLBatchRequest::Single(GraphQLRequest { + query: "{ add(a: 2, b: 3) }".to_string(), + operation_name: None, + variables: None, + extensions: None, + })); assert_eq!(do_from_request(req).await, expected); } diff --git a/juniper_hyper/src/lib.rs b/juniper_hyper/src/lib.rs index 41d408490..1337ac523 100644 --- a/juniper_hyper/src/lib.rs +++ b/juniper_hyper/src/lib.rs @@ -138,9 +138,12 @@ where let query = String::from_utf8(chunk.to_bytes().into()).map_err(GraphQLRequestError::BodyUtf8)?; - Ok(GraphQLBatchRequest::Single(GraphQLRequest::new( - query, None, None, - ))) + Ok(GraphQLBatchRequest::Single(GraphQLRequest { + query, + operation_name: None, + variables: None, + extensions: None, + })) } /// Generates a [`Response`] page containing [GraphiQL]. @@ -284,7 +287,12 @@ where } } match query { - Some(query) => Ok(JuniperGraphQLRequest::new(query, operation_name, variables)), + Some(query) => Ok(JuniperGraphQLRequest { + query, + operation_name, + variables, + extensions: None, + }), None => Err(GraphQLRequestError::Invalid( "'query' parameter is missing".into(), )), diff --git a/juniper_rocket/src/lib.rs b/juniper_rocket/src/lib.rs index bdff94bad..9705540a7 100644 --- a/juniper_rocket/src/lib.rs +++ b/juniper_rocket/src/lib.rs @@ -354,7 +354,12 @@ where match ctx.errors.is_empty() { true => Ok(GraphQLRequest(GraphQLBatchRequest::Single( - http::GraphQLRequest::new(ctx.query.unwrap(), ctx.operation_name, ctx.variables), + http::GraphQLRequest { + query: ctx.query.unwrap(), + operation_name: ctx.operation_name, + variables: ctx.variables, + extensions: None, + }, ))), false => Err(ctx.errors), } @@ -402,7 +407,12 @@ where Err(e) => return Outcome::Error((Status::BadRequest, e.to_string())), } } else { - GraphQLBatchRequest::Single(http::GraphQLRequest::new(body, None, None)) + GraphQLBatchRequest::Single(http::GraphQLRequest { + query: body, + operation_name: None, + variables: None, + extensions: None, + }) })) }) .await @@ -538,9 +548,12 @@ mod fromform_tests { assert!(result.is_ok()); let variables = ::serde_json::from_str::(r#"{"foo":"bar"}"#).unwrap(); - let expected = GraphQLRequest(http::GraphQLBatchRequest::Single( - http::GraphQLRequest::new("test".into(), None, Some(variables)), - )); + let expected = GraphQLRequest(http::GraphQLBatchRequest::Single(http::GraphQLRequest { + query: "test".into(), + operation_name: None, + variables: Some(variables), + extensions: None, + })); assert_eq!(result.unwrap(), expected); } @@ -551,9 +564,12 @@ mod fromform_tests { r#"query=test&variables={"foo":"x%20y%26%3F+z"}"#, )); let variables = ::serde_json::from_str::(r#"{"foo":"x y&? z"}"#).unwrap(); - let expected = GraphQLRequest(http::GraphQLBatchRequest::Single( - http::GraphQLRequest::new("test".into(), None, Some(variables)), - )); + let expected = GraphQLRequest(http::GraphQLBatchRequest::Single(http::GraphQLRequest { + query: "test".into(), + operation_name: None, + variables: Some(variables), + extensions: None, + })); assert_eq!(result.unwrap(), expected); } @@ -566,9 +582,12 @@ mod fromform_tests { assert!(result.is_ok()); - let expected = GraphQLRequest(http::GraphQLBatchRequest::Single( - http::GraphQLRequest::new("%foo bar baz&?".into(), Some("test".into()), None), - )); + let expected = GraphQLRequest(http::GraphQLBatchRequest::Single(http::GraphQLRequest { + query: "%foo bar baz&?".into(), + operation_name: "test".into(), + variables: None, + extensions: None, + })); assert_eq!(result.unwrap(), expected); } diff --git a/juniper_warp/src/lib.rs b/juniper_warp/src/lib.rs index 35e3e4b2c..0d10b9671 100644 --- a/juniper_warp/src/lib.rs +++ b/juniper_warp/src/lib.rs @@ -305,7 +305,12 @@ where .and_then(async |body: Bytes| { let query = str::from_utf8(body.as_ref()) .map_err(|e| reject::custom(FilterError::NonUtf8Body(e)))?; - let req = GraphQLRequest::new(query.into(), None, None); + let req = GraphQLRequest { + query: query.into(), + operation_name: None, + variables: None, + extensions: None, + }; Ok::, Rejection>(GraphQLBatchRequest::Single(req)) }) } @@ -319,15 +324,18 @@ where warp::get() .and(query::query()) .and_then(async |mut qry: HashMap| { - let req = GraphQLRequest::new( - qry.remove("query") + let req = GraphQLRequest { + query: qry + .remove("query") .ok_or_else(|| reject::custom(FilterError::MissingPathQuery))?, - qry.remove("operation_name"), - qry.remove("variables") + operation_name: qry.remove("operation_name"), + variables: qry + .remove("variables") .map(|vs| serde_json::from_str(&vs)) .transpose() .map_err(|e| reject::custom(FilterError::InvalidPathVariables(e)))?, - ); + extensions: todo!(), + }; Ok::, Rejection>(GraphQLBatchRequest::Single(req)) }) } From 927dd9a0523f391c1213773ba19039671936d41a Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 24 Oct 2025 19:10:30 +0300 Subject: [PATCH 4/7] Bootstrap injection [skip ci] --- juniper/src/executor/mod.rs | 90 ++++++++++++++++++++++++++++++++++++- juniper/src/http/mod.rs | 60 ++++++++++++------------- 2 files changed, 119 insertions(+), 31 deletions(-) diff --git a/juniper/src/executor/mod.rs b/juniper/src/executor/mod.rs index 33a2477d0..42779800a 100644 --- a/juniper/src/executor/mod.rs +++ b/juniper/src/executor/mod.rs @@ -396,7 +396,95 @@ pub trait FromContext { } /// Marker trait for types that can act as context objects for `GraphQL` types. -pub trait Context {} +pub trait Context { + /// Consumes the provided [request `extensions`][0] into this [`Context`]. + /// + /// # Implementation + /// + /// Default implementation does nothing. + /// + /// This method should be implemented if a [`Context`] implementation wants to be populated with + /// [request `extensions`][0]. Since [request `extensions`][0] could be modeled as an arbitrary + /// type, the implementation should downcast to the desired type before use. + /// + /// ```rust + /// # use juniper::{ + /// # Context, DefaultScalarValue, EmptyMutation, EmptySubscription, RootNode, + /// # graphql_object, graphql_value, http::GraphQLRequest, + /// # }; + /// # use serde::{Deserialize, Serialize}; + /// # use serde_json::json; + /// # + /// #[derive(Deserialize, Serialize)] + /// #[serde(rename_all = "camelCase")] + /// struct CustomExtensions { + /// persisted_query: PersistedQueryExtensions, + /// } + /// + /// #[derive(Deserialize, Serialize)] + /// #[serde(rename_all = "camelCase")] + /// struct PersistedQueryExtensions { + /// sha256_hash: Box, + /// } + /// + /// type CustomGraphQLRequest = GraphQLRequest; + /// + /// #[derive(Default)] + /// struct CustomContext { + /// persisted_query_sha256_hash: Option>, + /// } + /// + /// impl Context for CustomContext { + /// fn consume_request_extensions(&mut self, extensions: &T) { + /// use juniper::AnyExt as _; // allows downcasting directly on types without `dyn` + /// + /// if let Some(ext) = extensions.downcast_ref::() { + /// self.persisted_query_sha256_hash = + /// Some(ext.persisted_query.sha256_hash.clone()); + /// } + /// } + /// } + /// + /// struct Query; + /// + /// #[graphql_object] + /// impl Query { + /// fn is_persisted_query(context: &CustomContext) -> bool { + /// context.persisted_query_sha256_hash.is_some() + /// } + /// } + /// # + /// # type Schema = RootNode< + /// # Query, EmptyMutation, EmptySubscription, + /// # >; + /// # + /// # #[tokio::main] + /// # async fn main() { + /// # let request: CustomGraphQLRequest = serde_json::from_value(json!({ + /// # "query": "{ isPersistedQuery }", + /// # "extensions": { + /// # "persistedQuery": { + /// # "sha256Hash": + /// # "c205cf782b5c43c3fc67b5233445b78fbea47b99a0302cf31bda2a8e2162e1e6", + /// # }, + /// # }, + /// # })).unwrap(); + /// # let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()); + /// # let context = CustomContext::default(); + /// # + /// # assert_eq!( + /// # request.execute(&schema, context).await.into_result(), + /// # Ok((graphql_value!({"isPersistedQuery": true}), vec![])), + /// # ); + /// # } + /// ``` + /// + /// [0]: https://spec.graphql.org/September2025#sel-FANHLBBgBBvC0vW + fn consume_request_extensions(&mut self, extensions: &T) { + _ = extensions; + } + +} impl Context for &C {} diff --git a/juniper/src/http/mod.rs b/juniper/src/http/mod.rs index 82ff17f69..92230b318 100644 --- a/juniper/src/http/mod.rs +++ b/juniper/src/http/mod.rs @@ -9,6 +9,7 @@ use serde::{ }; use crate::{ + Context, FieldError, GraphQLError, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, RootNode, Value, Variables, ast::InputValue, @@ -74,21 +75,22 @@ where pub fn execute_sync( &self, root_node: &RootNode, - context: &QueryT::Context, + mut context: QueryT::Context, ) -> GraphQLResponse where S: ScalarValue, - QueryT: GraphQLType, + QueryT: GraphQLType, MutationT: GraphQLType, SubscriptionT: GraphQLType, + Ext: 'static, { - GraphQLResponse(crate::execute_sync( - &self.query, - self.operation_name.as_deref(), - root_node, - &self.variables(), - context, - )) + let op = self.operation_name.as_deref(); + let vars = &self.variables(); + if let Some(ext) = self.extensions.as_ref() { + context.consume_request_extensions(ext) + } + let res = crate::execute_sync(&self.query, op, root_node, vars, &context); + GraphQLResponse(res) } /// Execute a GraphQL request using the specified schema and context @@ -98,21 +100,21 @@ where pub async fn execute<'a, QueryT, MutationT, SubscriptionT>( &'a self, root_node: &'a RootNode, - context: &'a QueryT::Context, + mut context: QueryT::Context, ) -> GraphQLResponse where - QueryT: GraphQLTypeAsync, - QueryT::TypeInfo: Sync, - QueryT::Context: Sync, - MutationT: GraphQLTypeAsync, - MutationT::TypeInfo: Sync, - SubscriptionT: GraphQLType + Sync, - SubscriptionT::TypeInfo: Sync, S: ScalarValue + Send + Sync, + QueryT: GraphQLTypeAsync, + MutationT: GraphQLTypeAsync, + SubscriptionT: GraphQLType + Sync, + Ext: 'static, { let op = self.operation_name.as_deref(); let vars = &self.variables(); - let res = crate::execute(&self.query, op, root_node, vars, context).await; + if let Some(ext) = self.extensions.as_ref() { + context.consume_request_extensions(ext) + } + let res = crate::execute(&self.query, op, root_node, vars, &context).await; GraphQLResponse(res) } } @@ -163,18 +165,19 @@ where pub fn execute_sync<'a, QueryT, MutationT, SubscriptionT>( &'a self, root_node: &'a RootNode, - context: &QueryT::Context, + context: QueryT::Context, ) -> GraphQLBatchResponse where - QueryT: GraphQLType, + QueryT: GraphQLType, MutationT: GraphQLType, SubscriptionT: GraphQLType, + Ext: 'static, { match self { Self::Single(req) => GraphQLBatchResponse::Single(req.execute_sync(root_node, context)), Self::Batch(reqs) => GraphQLBatchResponse::Batch( reqs.iter() - .map(|req| req.execute_sync(root_node, context)) + .map(|req| req.execute_sync(root_node, context.clone())) .collect(), ), } @@ -187,17 +190,14 @@ where pub async fn execute<'a, QueryT, MutationT, SubscriptionT>( &'a self, root_node: &'a RootNode, - context: &'a QueryT::Context, + context: QueryT::Context, ) -> GraphQLBatchResponse where - QueryT: GraphQLTypeAsync, - QueryT::TypeInfo: Sync, - QueryT::Context: Sync, - MutationT: GraphQLTypeAsync, - MutationT::TypeInfo: Sync, - SubscriptionT: GraphQLSubscriptionType, - SubscriptionT::TypeInfo: Sync, S: Send + Sync, + QueryT: GraphQLTypeAsync, + MutationT: GraphQLTypeAsync, + SubscriptionT: GraphQLSubscriptionType, + Ext: 'static, { match self { Self::Single(req) => { @@ -206,7 +206,7 @@ where } Self::Batch(reqs) => { let resps = futures::future::join_all( - reqs.iter().map(|req| req.execute(root_node, context)), + reqs.iter().map(|req| req.execute(root_node, context.clone())), ) .await; GraphQLBatchResponse::Batch(resps) From 54ee360e2daf4db37574849b09088871b54d6028 Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 27 Oct 2025 20:30:07 +0200 Subject: [PATCH 5/7] Impl in `juniper_actix`, vol.1 [skip ci] --- book/src/serve/index.md | 2 +- juniper/src/executor/mod.rs | 3 +- juniper/src/http/mod.rs | 8 +- juniper_actix/Cargo.toml | 5 +- juniper_actix/examples/subscription.rs | 14 +- juniper_actix/src/lib.rs | 652 ++++++++++++++++--------- 6 files changed, 432 insertions(+), 252 deletions(-) diff --git a/book/src/serve/index.md b/book/src/serve/index.md index 75dd7f8c2..87eb58153 100644 --- a/book/src/serve/index.md +++ b/book/src/serve/index.md @@ -54,7 +54,7 @@ In the [Juniper] ecosystem, both implementations are provided by the [`juniper_g [`subscriptions-transport-ws` npm package]: https://npmjs.com/package/subscriptions-transport-ws [`warp`]: https://docs.rs/warp [Apollo]: https://www.apollographql.com -[GraphiQL]: https://github.com/graphql/graphiql +ยง [GraphQL]: https://graphql.org [GraphQL Playground]: https://github.com/prisma/graphql-playground [HTTP]: https://en.wikipedia.org/wiki/HTTP diff --git a/juniper/src/executor/mod.rs b/juniper/src/executor/mod.rs index 42779800a..4be5bae0c 100644 --- a/juniper/src/executor/mod.rs +++ b/juniper/src/executor/mod.rs @@ -395,7 +395,7 @@ pub trait FromContext { fn from(value: &T) -> &Self; } -/// Marker trait for types that can act as context objects for `GraphQL` types. +/// Execution context for resolving GraphQL types. pub trait Context { /// Consumes the provided [request `extensions`][0] into this [`Context`]. /// @@ -483,7 +483,6 @@ pub trait Context { fn consume_request_extensions(&mut self, extensions: &T) { _ = extensions; } - } impl Context for &C {} diff --git a/juniper/src/http/mod.rs b/juniper/src/http/mod.rs index 92230b318..8836189a3 100644 --- a/juniper/src/http/mod.rs +++ b/juniper/src/http/mod.rs @@ -9,9 +9,8 @@ use serde::{ }; use crate::{ - Context, - FieldError, GraphQLError, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, RootNode, - Value, Variables, + Context, FieldError, GraphQLError, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, + RootNode, Value, Variables, ast::InputValue, executor::{ExecutionError, ValuesStream}, value::{DefaultScalarValue, ScalarValue}, @@ -206,7 +205,8 @@ where } Self::Batch(reqs) => { let resps = futures::future::join_all( - reqs.iter().map(|req| req.execute(root_node, context.clone())), + reqs.iter() + .map(|req| req.execute(root_node, context.clone())), ) .await; GraphQLBatchResponse::Batch(resps) diff --git a/juniper_actix/Cargo.toml b/juniper_actix/Cargo.toml index 7f93673d6..f491f7a0c 100644 --- a/juniper_actix/Cargo.toml +++ b/juniper_actix/Cargo.toml @@ -23,8 +23,8 @@ rustdoc-args = ["--cfg", "docsrs"] [features] subscriptions = [ - "dep:actix-ws", - "dep:derive_more", + "dep:actix-ws", + "dep:derive_more", "dep:futures", "dep:juniper_graphql_ws", ] @@ -47,6 +47,7 @@ actix-test = "0.1" anyhow = "1.0.47" async-stream = "0.3" env_logger = "0.11" +futures = "0.3" juniper = { version = "0.17", path = "../juniper", features = ["expose-test-schema"] } log = "0.4" rand = "0.9" diff --git a/juniper_actix/examples/subscription.rs b/juniper_actix/examples/subscription.rs index 001aa70a2..868b07df1 100644 --- a/juniper_actix/examples/subscription.rs +++ b/juniper_actix/examples/subscription.rs @@ -6,17 +6,14 @@ use std::{pin::Pin, time::Duration}; use actix_cors::Cors; use actix_web::{ - App, Error, HttpRequest, HttpResponse, HttpServer, Responder, - http::header, - middleware, - web::{self, Data}, + App, Error, HttpRequest, HttpResponse, HttpServer, Responder, http::header, middleware, web, }; use juniper::{ EmptyMutation, FieldError, GraphQLObject, RootNode, graphql_subscription, graphql_value, tests::fixtures::starwars::schema::{Database, Query}, }; -use juniper_actix::{graphiql_handler, graphql_handler, playground_handler, subscriptions}; +use juniper_actix::{GraphQL, graphiql_handler, playground_handler, subscriptions}; use juniper_graphql_ws::ConnectionConfig; type Schema = RootNode, Subscription>; @@ -36,10 +33,9 @@ async fn graphiql() -> Result { async fn graphql( req: HttpRequest, payload: web::Payload, - schema: Data, + schema: web::Data, ) -> Result { - let context = Database::new(); - graphql_handler(&schema, &context, req, payload).await + GraphQL::handler(&schema, Database::new(), req, payload).await } async fn homepage() -> impl Responder { @@ -127,7 +123,7 @@ async fn main() -> std::io::Result<()> { HttpServer::new(move || { App::new() - .app_data(Data::new(schema())) + .app_data(web::Data::new(schema())) .wrap( Cors::default() .allow_any_origin() diff --git a/juniper_actix/src/lib.rs b/juniper_actix/src/lib.rs index 17580901b..cb4bb146f 100644 --- a/juniper_actix/src/lib.rs +++ b/juniper_actix/src/lib.rs @@ -3,18 +3,113 @@ #![cfg_attr(not(any(doc, test)), doc = env!("CARGO_PKG_NAME"))] #![cfg_attr(test, expect(unused_crate_dependencies, reason = "examples"))] +use std::marker::PhantomData; + use actix_web::{ Error, FromRequest, HttpMessage, HttpRequest, HttpResponse, error::JsonPayloadError, http::Method, web, }; use juniper::{ - ScalarValue, + Context, DefaultScalarValue, GraphQLSubscriptionType, GraphQLTypeAsync, ScalarValue, http::{ GraphQLBatchRequest, GraphQLRequest, graphiql::graphiql_source, playground::playground_source, }, }; -use serde::Deserialize; +use serde::{Deserialize, de::DeserializeOwned}; + +pub type GraphQL = GraphQLWith; + +pub struct GraphQLWith>( + PhantomData<(S, RequestExtensions)>, +); + +impl GraphQLWith +where + S: ScalarValue + Send + Sync, + RequestExtensions: DeserializeOwned + 'static, +{ + pub async fn handler( + schema: &juniper::RootNode, + context: Query::Context, + req: HttpRequest, + payload: web::Payload, + ) -> Result + where + Query: GraphQLTypeAsync, + Mutation: GraphQLTypeAsync, + Subscription: GraphQLSubscriptionType, + S: ScalarValue + Send + Sync, + { + match *req.method() { + Method::POST => post_graphql_handler(schema, context, req, payload).await, + Method::GET => get_graphql_handler(schema, context, req).await, + _ => Err(actix_web::error::UrlGenerationError::ResourceNotFound.into()), + } + } + + pub async fn get_handler( + schema: &juniper::RootNode, + context: Query::Context, + req: HttpRequest, + ) -> Result + where + Query: GraphQLTypeAsync, + Mutation: GraphQLTypeAsync, + Subscription: GraphQLSubscriptionType, + { + let get_req = web::Query::::from_query(req.query_string())?; + let req = GraphQLRequest::::try_from(get_req.into_inner())?; + let gql_response = req.execute(schema, context).await; + let body_response = serde_json::to_string(&gql_response)?; + let mut response = match gql_response.is_ok() { + true => HttpResponse::Ok(), + false => HttpResponse::BadRequest(), + }; + Ok(response + .content_type("application/json") + .body(body_response)) + } + + pub async fn post_handler( + schema: &juniper::RootNode, + context: Query::Context, + req: HttpRequest, + payload: web::Payload, + ) -> Result + where + Query: GraphQLTypeAsync, + Mutation: GraphQLTypeAsync, + Subscription: GraphQLSubscriptionType, + { + let req = match req.content_type() { + "application/json" => { + let body = String::from_request(&req, &mut payload.into_inner()).await?; + serde_json::from_str::>(&body) + .map_err(JsonPayloadError::Deserialize) + } + "application/graphql" => { + let body = String::from_request(&req, &mut payload.into_inner()).await?; + Ok(GraphQLBatchRequest::::Single( + GraphQLRequest { + query: body, + operation_name: None, + variables: None, + extensions: None, + }, + )) + } + _ => Err(JsonPayloadError::ContentType), + }?; + let gql_batch_response = req.execute(schema, context).await; + let gql_response = serde_json::to_string(&gql_batch_response)?; + let mut response = match gql_batch_response.is_ok() { + true => HttpResponse::Ok(), + false => HttpResponse::BadRequest(), + }; + Ok(response.content_type("application/json").body(gql_response)) + } +} #[derive(Deserialize, Clone, PartialEq, Debug)] #[serde(deny_unknown_fields)] @@ -23,134 +118,103 @@ struct GetGraphQLRequest { #[serde(rename = "operationName")] operation_name: Option, variables: Option, + #[serde(default)] + extensions: Option, } -impl From for GraphQLRequest +impl TryFrom for GraphQLRequest where S: ScalarValue, + Ext: DeserializeOwned, { - fn from(get_req: GetGraphQLRequest) -> Self { + type Error = JsonPayloadError; + + fn try_from(value: GetGraphQLRequest) -> Result { let GetGraphQLRequest { query, operation_name, variables, - } = get_req; - let variables = variables.map(|s| serde_json::from_str(&s).unwrap()); - Self { + extensions, + } = value; + Ok(Self { query, operation_name, - variables, - extensions: None, - } + variables: variables + .as_deref() + .map(serde_json::from_str) + .transpose() + .map_err(JsonPayloadError::Deserialize)?, + extensions: extensions + .as_deref() + .map(serde_json::from_str) + .transpose() + .map_err(JsonPayloadError::Deserialize)?, + }) } } /// Actix Web GraphQL Handler for GET and POST requests -pub async fn graphql_handler( +pub async fn graphql_handler( schema: &juniper::RootNode, - context: &CtxT, + context: Query::Context, req: HttpRequest, - payload: actix_web::web::Payload, + payload: web::Payload, ) -> Result where - Query: juniper::GraphQLTypeAsync, - Query::TypeInfo: Sync, - Mutation: juniper::GraphQLTypeAsync, - Mutation::TypeInfo: Sync, - Subscription: juniper::GraphQLSubscriptionType, - Subscription::TypeInfo: Sync, - CtxT: Sync, + Query: GraphQLTypeAsync, + Mutation: GraphQLTypeAsync, + Subscription: GraphQLSubscriptionType, S: ScalarValue + Send + Sync, { - match *req.method() { - Method::POST => post_graphql_handler(schema, context, req, payload).await, - Method::GET => get_graphql_handler(schema, context, req).await, - _ => Err(actix_web::error::UrlGenerationError::ResourceNotFound.into()), - } + GraphQLWith::::handler(schema, context, req, payload).await } + /// Actix GraphQL Handler for GET requests -pub async fn get_graphql_handler( +pub async fn get_graphql_handler( schema: &juniper::RootNode, - context: &CtxT, + context: Query::Context, req: HttpRequest, ) -> Result where - Query: juniper::GraphQLTypeAsync, - Query::TypeInfo: Sync, - Mutation: juniper::GraphQLTypeAsync, - Mutation::TypeInfo: Sync, - Subscription: juniper::GraphQLSubscriptionType, - Subscription::TypeInfo: Sync, - CtxT: Sync, + Query: GraphQLTypeAsync, + Mutation: GraphQLTypeAsync, + Subscription: GraphQLSubscriptionType, S: ScalarValue + Send + Sync, { - let get_req = web::Query::::from_query(req.query_string())?; - let req = GraphQLRequest::from(get_req.into_inner()); - let gql_response = req.execute(schema, context).await; - let body_response = serde_json::to_string(&gql_response)?; - let mut response = match gql_response.is_ok() { - true => HttpResponse::Ok(), - false => HttpResponse::BadRequest(), - }; - Ok(response - .content_type("application/json") - .body(body_response)) + GraphQLWith::::get_handler(schema, context, req).await } /// Actix GraphQL Handler for POST requests -pub async fn post_graphql_handler( +pub async fn post_graphql_handler( schema: &juniper::RootNode, - context: &CtxT, + context: Query::Context, req: HttpRequest, - payload: actix_web::web::Payload, + payload: web::Payload, ) -> Result where - Query: juniper::GraphQLTypeAsync, - Query::TypeInfo: Sync, - Mutation: juniper::GraphQLTypeAsync, - Mutation::TypeInfo: Sync, - Subscription: juniper::GraphQLSubscriptionType, - Subscription::TypeInfo: Sync, - CtxT: Sync, + Query: GraphQLTypeAsync, + Mutation: GraphQLTypeAsync, + Subscription: GraphQLSubscriptionType, S: ScalarValue + Send + Sync, { - let req = match req.content_type() { - "application/json" => { - let body = String::from_request(&req, &mut payload.into_inner()).await?; - serde_json::from_str::>(&body) - .map_err(JsonPayloadError::Deserialize) - } - "application/graphql" => { - let body = String::from_request(&req, &mut payload.into_inner()).await?; - Ok(GraphQLBatchRequest::Single(GraphQLRequest { - query: body, - operation_name: None, - variables: None, - extensions: None, - })) - } - _ => Err(JsonPayloadError::ContentType), - }?; - let gql_batch_response = req.execute(schema, context).await; - let gql_response = serde_json::to_string(&gql_batch_response)?; - let mut response = match gql_batch_response.is_ok() { - true => HttpResponse::Ok(), - false => HttpResponse::BadRequest(), - }; - Ok(response.content_type("application/json").body(gql_response)) + GraphQLWith::::post_handler(schema, context, req, payload).await } -/// Create a handler that replies with an HTML page containing GraphiQL. This does not handle routing, so you can mount it on any endpoint +/// Creates a handler that replies with an HTML page containing [GraphiQL]. /// -/// For example: +/// This does not handle routing, so you can mount it on any endpoint. /// -/// ``` +/// # Example +/// +/// ```rust /// # use juniper_actix::graphiql_handler; /// # use actix_web::{web, App}; /// /// let app = App::new() -/// .route("/", web::get().to(|| graphiql_handler("/graphql", Some("/graphql/subscriptions")))); +/// .route("/", web::get().to(|| graphiql_handler("/graphql", Some("/graphql/subscriptions")))); /// ``` +/// +/// [GraphiQL]: https://github.com/graphql/graphiql pub async fn graphiql_handler( graphql_endpoint_url: &str, subscriptions_endpoint_url: Option<&'static str>, @@ -161,7 +225,11 @@ pub async fn graphiql_handler( .body(html)) } -/// Create a handler that replies with an HTML page containing GraphQL Playground. This does not handle routing, so you cant mount it on any endpoint. +/// Create a handler that replies with an HTML page containing [GraphQL Playground]. +/// +/// This does not handle routing, so you cant mount it on any endpoint. +/// +/// [GraphQL Playground]: https://github.com/prisma/graphql-playground pub async fn playground_handler( graphql_endpoint_url: &str, subscriptions_endpoint_url: Option<&'static str>, @@ -177,6 +245,7 @@ pub async fn playground_handler( pub mod subscriptions { use std::{pin::pin, sync::Arc}; + use crate::GraphQLWith; use actix_web::{ HttpRequest, HttpResponse, http::header::{HeaderName, HeaderValue}, @@ -185,7 +254,220 @@ pub mod subscriptions { use derive_more::with_trait::{Display, Error as StdError}; use futures::{SinkExt as _, StreamExt as _, future}; use juniper::{GraphQLSubscriptionType, GraphQLTypeAsync, RootNode, ScalarValue}; - use juniper_graphql_ws::{ArcSchema, Init, graphql_transport_ws, graphql_ws}; + use juniper_graphql_ws::{ArcSchema, Init, Schema, graphql_transport_ws, graphql_ws}; + + impl GraphQLWith + where + S: ScalarValue + Send + Sync + 'static, + { + /// Serves by auto-selecting between the + /// [legacy `graphql-ws` GraphQL over WebSocket Protocol][old] and the + /// [new `graphql-transport-ws` GraphQL over WebSocket Protocol][new], based on the + /// `Sec-Websocket-Protocol` HTTP header value. + /// + /// The `schema` argument is your [`juniper`] schema. + /// + /// The `init` argument is used to provide the custom [`juniper::Context`] and additional + /// configuration for connections. This can be a [`juniper_graphql_ws::ConnectionConfig`] if + /// the context and configuration are already known, or it can be a closure that gets + /// executed asynchronously whenever a client sends the subscription initialization message. + /// Using a closure allows to perform an authentication based on the parameters provided by + /// a client. + /// + /// [new]: https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md + /// [old]: https://github.com/apollographql/subscriptions-transport-ws/blob/v0.11.0/PROTOCOL.md + pub async fn auto_ws_handler( + req: HttpRequest, + stream: web::Payload, + schema: Arc>, + init: I, + ) -> Result + where + Query: GraphQLTypeAsync + + Send + + 'static, + Mutation: GraphQLTypeAsync + + Send + + 'static, + Subscription: GraphQLSubscriptionType + + Send + + 'static, + I: Init + Send, + { + if req + .headers() + .get("sec-websocket-protocol") + .map(AsRef::as_ref) + == Some("graphql-ws".as_bytes()) + { + graphql_ws_handler(req, stream, schema, init).await + } else { + graphql_transport_ws_handler(req, stream, schema, init).await + } + } + + /// Serves the [legacy `graphql-ws` GraphQL over WebSocket Protocol][old]. + /// + /// The `init` argument is used to provide the context and additional configuration for + /// connections. This can be a [`juniper_graphql_ws::ConnectionConfig`] if the context and + /// configuration are already known, or it can be a closure that gets executed + /// asynchronously when the client sends the `GQL_CONNECTION_INIT` message. Using a closure + /// allows to perform an authentication based on the parameters provided by a client. + /// + /// > __WARNING__: This protocol has been deprecated in favor of the + /// > [new `graphql-transport-ws` GraphQL over WebSocket Protocol][new], which + /// > is provided by the [`transport_ws_handler()`] method. + /// + /// [new]: https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md + /// [old]: https://github.com/apollographql/subscriptions-transport-ws/blob/v0.11.0/PROTOCOL.md + /// [`transport_ws_handler()`]: Self::transport_ws_handler + pub async fn ws_handler( + req: HttpRequest, + stream: web::Payload, + schema: Arc>, + init: I, + ) -> Result + where + Query: GraphQLTypeAsync + + Send + + 'static, + Mutation: GraphQLTypeAsync + + Send + + 'static, + Subscription: GraphQLSubscriptionType + + Send + + 'static, + I: Init + Send, + { + let (mut resp, mut ws_tx, ws_rx) = actix_ws::handle(&req, stream)?; + let (s_tx, mut s_rx) = graphql_ws::Connection::new(ArcSchema(schema), init).split(); + + actix_web::rt::spawn(async move { + let input = ws_rx + .map(|r| r.map(Message)) + .forward(s_tx.sink_map_err(|e| match e {})); + let output = pin!(async move { + while let Some(msg) = s_rx.next().await { + match serde_json::to_string(&msg) { + Ok(m) => { + if ws_tx.text(m).await.is_err() { + return; + } + } + Err(e) => { + _ = ws_tx + .close(Some(actix_ws::CloseReason { + code: actix_ws::CloseCode::Error, + description: Some(format!( + "error serializing response: {e}" + )), + })) + .await; + return; + } + } + } + _ = ws_tx + .close(Some((actix_ws::CloseCode::Normal, "Normal Closure").into())) + .await; + }); + + // No errors can be returned here, so ignoring is OK. + _ = future::select(input, output).await; + }); + + resp.headers_mut().insert( + HeaderName::from_static("sec-websocket-protocol"), + HeaderValue::from_static("graphql-ws"), + ); + Ok(resp) + } + + /// Serves the [new `graphql-transport-ws` GraphQL over WebSocket Protocol][new]. + /// + /// The `init` argument is used to provide the context and additional configuration for + /// connections. This can be a [`juniper_graphql_ws::ConnectionConfig`] if the context and + /// configuration are already known, or it can be a closure that gets executed + /// asynchronously when the client sends the `ConnectionInit` message. Using a closure + /// allows to perform an authentication based on the parameters provided by a client. + /// + /// [new]: https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md + pub async fn transport_ws_handler( + req: HttpRequest, + stream: web::Payload, + schema: Arc>, + init: I, + ) -> Result + where + Query: GraphQLTypeAsync + + Send + + 'static, + Mutation: GraphQLTypeAsync + + Send + + 'static, + Subscription: GraphQLSubscriptionType + + Send + + 'static, + I: Init + Send, + { + let (mut resp, mut ws_tx, ws_rx) = actix_ws::handle(&req, stream)?; + let (s_tx, mut s_rx) = + graphql_transport_ws::Connection::new(ArcSchema(schema), init).split(); + + actix_web::rt::spawn(async move { + let input = ws_rx + .map(|r| r.map(Message)) + .forward(s_tx.sink_map_err(|e| match e {})); + let output = pin!(async move { + while let Some(output) = s_rx.next().await { + match output { + graphql_transport_ws::Output::Message(msg) => { + match serde_json::to_string(&msg) { + Ok(m) => { + if ws_tx.text(m).await.is_err() { + return; + } + } + Err(e) => { + _ = ws_tx + .close(Some(actix_ws::CloseReason { + code: actix_ws::CloseCode::Error, + description: Some(format!( + "error serializing response: {e}", + )), + })) + .await; + return; + } + } + } + graphql_transport_ws::Output::Close { code, message } => { + _ = ws_tx + .close(Some(actix_ws::CloseReason { + code: code.into(), + description: Some(message), + })) + .await; + return; + } + } + } + _ = ws_tx + .close(Some((actix_ws::CloseCode::Normal, "Normal Closure").into())) + .await; + }); + + // No errors can be returned here, so ignoring is OK. + _ = future::select(input, output).await; + }); + + resp.headers_mut().insert( + HeaderName::from_static("sec-websocket-protocol"), + HeaderValue::from_static("graphql-transport-ws"), + ); + Ok(resp) + } + } /// Serves by auto-selecting between the /// [legacy `graphql-ws` GraphQL over WebSocket Protocol][old] and the @@ -202,33 +484,25 @@ pub mod subscriptions { /// /// [new]: https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md /// [old]: https://github.com/apollographql/subscriptions-transport-ws/blob/v0.11.0/PROTOCOL.md - pub async fn ws_handler( + pub async fn ws_handler( req: HttpRequest, stream: web::Payload, schema: Arc>, init: I, ) -> Result where - Query: GraphQLTypeAsync + Send + 'static, - Query::TypeInfo: Send + Sync, - Mutation: GraphQLTypeAsync + Send + 'static, - Mutation::TypeInfo: Send + Sync, - Subscription: GraphQLSubscriptionType + Send + 'static, - Subscription::TypeInfo: Send + Sync, - CtxT: Unpin + Send + Sync + 'static, + Query: GraphQLTypeAsync + + Send + + 'static, + Mutation: + GraphQLTypeAsync + Send + 'static, + Subscription: GraphQLSubscriptionType + + Send + + 'static, S: ScalarValue + Send + Sync + 'static, - I: Init + Send, + I: Init + Send, { - if req - .headers() - .get("sec-websocket-protocol") - .map(AsRef::as_ref) - == Some("graphql-ws".as_bytes()) - { - graphql_ws_handler(req, stream, schema, init).await - } else { - graphql_transport_ws_handler(req, stream, schema, init).await - } + GraphQLWith::::auto_ws_handler(req, stream, schema, init).await } /// Serves the [legacy `graphql-ws` GraphQL over WebSocket Protocol][old]. @@ -245,63 +519,25 @@ pub mod subscriptions { /// /// [new]: https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md /// [old]: https://github.com/apollographql/subscriptions-transport-ws/blob/v0.11.0/PROTOCOL.md - pub async fn graphql_ws_handler( + pub async fn graphql_ws_handler( req: HttpRequest, stream: web::Payload, schema: Arc>, init: I, ) -> Result where - Query: GraphQLTypeAsync + Send + 'static, - Query::TypeInfo: Send + Sync, - Mutation: GraphQLTypeAsync + Send + 'static, - Mutation::TypeInfo: Send + Sync, - Subscription: GraphQLSubscriptionType + Send + 'static, - Subscription::TypeInfo: Send + Sync, - CtxT: Unpin + Send + Sync + 'static, + Query: GraphQLTypeAsync + + Send + + 'static, + Mutation: + GraphQLTypeAsync + Send + 'static, + Subscription: GraphQLSubscriptionType + + Send + + 'static, S: ScalarValue + Send + Sync + 'static, - I: Init + Send, + I: Init + Send, { - let (mut resp, mut ws_tx, ws_rx) = actix_ws::handle(&req, stream)?; - let (s_tx, mut s_rx) = graphql_ws::Connection::new(ArcSchema(schema), init).split(); - - actix_web::rt::spawn(async move { - let input = ws_rx - .map(|r| r.map(Message)) - .forward(s_tx.sink_map_err(|e| match e {})); - let output = pin!(async move { - while let Some(msg) = s_rx.next().await { - match serde_json::to_string(&msg) { - Ok(m) => { - if ws_tx.text(m).await.is_err() { - return; - } - } - Err(e) => { - _ = ws_tx - .close(Some(actix_ws::CloseReason { - code: actix_ws::CloseCode::Error, - description: Some(format!("error serializing response: {e}")), - })) - .await; - return; - } - } - } - _ = ws_tx - .close(Some((actix_ws::CloseCode::Normal, "Normal Closure").into())) - .await; - }); - - // No errors can be returned here, so ignoring is OK. - _ = future::select(input, output).await; - }); - - resp.headers_mut().insert( - HeaderName::from_static("sec-websocket-protocol"), - HeaderValue::from_static("graphql-ws"), - ); - Ok(resp) + GraphQLWith::::ws_handler(req, stream, schema, init).await } /// Serves the [new `graphql-transport-ws` GraphQL over WebSocket Protocol][new]. @@ -313,79 +549,25 @@ pub mod subscriptions { /// authentication based on the parameters provided by a client. /// /// [new]: https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md - pub async fn graphql_transport_ws_handler( + pub async fn graphql_transport_ws_handler( req: HttpRequest, stream: web::Payload, schema: Arc>, init: I, ) -> Result where - Query: GraphQLTypeAsync + Send + 'static, - Query::TypeInfo: Send + Sync, - Mutation: GraphQLTypeAsync + Send + 'static, - Mutation::TypeInfo: Send + Sync, - Subscription: GraphQLSubscriptionType + Send + 'static, - Subscription::TypeInfo: Send + Sync, - CtxT: Unpin + Send + Sync + 'static, + Query: GraphQLTypeAsync + + Send + + 'static, + Mutation: + GraphQLTypeAsync + Send + 'static, + Subscription: GraphQLSubscriptionType + + Send + + 'static, S: ScalarValue + Send + Sync + 'static, - I: Init + Send, + I: Init + Send, { - let (mut resp, mut ws_tx, ws_rx) = actix_ws::handle(&req, stream)?; - let (s_tx, mut s_rx) = - graphql_transport_ws::Connection::new(ArcSchema(schema), init).split(); - - actix_web::rt::spawn(async move { - let input = ws_rx - .map(|r| r.map(Message)) - .forward(s_tx.sink_map_err(|e| match e {})); - let output = pin!(async move { - while let Some(output) = s_rx.next().await { - match output { - graphql_transport_ws::Output::Message(msg) => { - match serde_json::to_string(&msg) { - Ok(m) => { - if ws_tx.text(m).await.is_err() { - return; - } - } - Err(e) => { - _ = ws_tx - .close(Some(actix_ws::CloseReason { - code: actix_ws::CloseCode::Error, - description: Some(format!( - "error serializing response: {e}", - )), - })) - .await; - return; - } - } - } - graphql_transport_ws::Output::Close { code, message } => { - _ = ws_tx - .close(Some(actix_ws::CloseReason { - code: code.into(), - description: Some(message), - })) - .await; - return; - } - } - } - _ = ws_tx - .close(Some((actix_ws::CloseCode::Normal, "Normal Closure").into())) - .await; - }); - - // No errors can be returned here, so ignoring is OK. - _ = future::select(input, output).await; - }); - - resp.headers_mut().insert( - HeaderName::from_static("sec-websocket-protocol"), - HeaderValue::from_static("graphql-transport-ws"), - ); - Ok(resp) + GraphQLWith::::transport_ws_handler(req, stream, schema, init).await } #[derive(Debug)] @@ -444,21 +626,24 @@ mod tests { use actix_http::body::MessageBody; use actix_web::{ - App, + App, Error, HttpRequest, HttpResponse, dev::ServiceResponse, http, http::header::{ACCEPT, CONTENT_TYPE}, test::{self, TestRequest}, - web::Data, + web, }; use futures::future; use juniper::{ EmptyMutation, EmptySubscription, - http::tests::{HttpIntegration, TestResponse, run_http_test_suite}, + http::{ + GraphQLBatchRequest, + tests::{HttpIntegration, TestResponse, run_http_test_suite}, + }, tests::fixtures::starwars::schema::{Database, Query}, }; - use super::*; + use super::{GraphQL, graphiql_handler, playground_handler}; type Schema = juniper::RootNode, EmptySubscription>; @@ -476,11 +661,10 @@ mod tests { async fn index( req: HttpRequest, - payload: actix_web::web::Payload, + payload: web::Payload, schema: web::Data, ) -> Result { - let context = Database::new(); - graphql_handler(&schema, &context, req, payload).await + GraphQL::handler(&schema, Database::new(), req, payload).await } #[actix_web::rt::test] @@ -521,11 +705,11 @@ mod tests { assert_eq!(resp.status(), http::StatusCode::OK); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(), - "text/html; charset=utf-8" + "text/html; charset=utf-8", ); let body = take_response_body_string(resp).await; assert!(body.contains("const JUNIPER_URL = '/dogs-api/graphql';")); - assert!(body.contains("const JUNIPER_SUBSCRIPTIONS_URL = '/dogs-api/subscriptions';")) + assert!(body.contains("const JUNIPER_SUBSCRIPTIONS_URL = '/dogs-api/subscriptions';")); } #[actix_web::rt::test] @@ -586,7 +770,7 @@ mod tests { let mut app = test::init_service( App::new() - .app_data(Data::new(schema)) + .app_data(web::Data::new(schema)) .route("/", web::post().to(index)), ) .await; @@ -599,7 +783,7 @@ mod tests { ); assert_eq!( take_response_body_string(resp).await, - r#"{"data":{"hero":{"name":"R2-D2"}}}"# + r#"{"data":{"hero":{"name":"R2-D2"}}}"#, ); } @@ -618,7 +802,7 @@ mod tests { let mut app = test::init_service( App::new() - .app_data(Data::new(schema)) + .app_data(web::Data::new(schema)) .route("/", web::get().to(index)), ) .await; @@ -632,7 +816,7 @@ mod tests { ); assert_eq!( take_response_body_string(resp).await, - r#"{"data":{"hero":{"name":"R2-D2"}}}"# + r#"{"data":{"hero":{"name":"R2-D2"}}}"#, ); } @@ -662,7 +846,7 @@ mod tests { let mut app = test::init_service( App::new() - .app_data(Data::new(schema)) + .app_data(web::Data::new(schema)) .route("/", web::post().to(index)), ) .await; @@ -676,7 +860,7 @@ mod tests { ); assert_eq!( take_response_body_string(resp).await, - r#"[{"data":{"hero":{"name":"R2-D2"}}},{"data":{"hero":{"id":"1000","name":"Luke Skywalker"}}}]"# + r#"[{"data":{"hero":{"name":"R2-D2"}}},{"data":{"hero":{"id":"1000","name":"Luke Skywalker"}}}]"#, ); } @@ -701,7 +885,7 @@ mod tests { let mut app = test::init_service( App::new() - .app_data(Data::new(schema)) + .app_data(web::Data::new(schema)) .route("/", web::to(index)), ) .await; From f70e28c84df6a59fdcdd3849a5f4985ceff62ed7 Mon Sep 17 00:00:00 2001 From: tyranron Date: Tue, 28 Oct 2025 17:44:33 +0200 Subject: [PATCH 6/7] Remake machinery for `Inject` trait --- juniper/src/executor/mod.rs | 185 ++++++++++++++++++------------------ juniper/src/http/mod.rs | 29 +++--- juniper/src/lib.rs | 2 +- 3 files changed, 105 insertions(+), 111 deletions(-) diff --git a/juniper/src/executor/mod.rs b/juniper/src/executor/mod.rs index 4be5bae0c..9ca755c30 100644 --- a/juniper/src/executor/mod.rs +++ b/juniper/src/executor/mod.rs @@ -379,6 +379,98 @@ where } } +/// Execution context for resolving GraphQL types. +pub trait Context {} + +impl Context for &C {} + +/// Injects the provided value into this [`Context`]. +/// +/// # Implementation +/// +/// Default implementation does nothing. +/// +/// This trait should be implemented for a [`Context`] implementation to be populated with +/// [request `extensions`][0]. +/// +/// ```rust +/// # use juniper::{ +/// # Context, DefaultScalarValue, EmptyMutation, EmptySubscription, Inject, RootNode, +/// # graphql_object, graphql_value, http::GraphQLRequest, +/// # }; +/// # use serde::{Deserialize, Serialize}; +/// # use serde_json::json; +/// # +/// #[derive(Deserialize, Serialize)] +/// #[serde(rename_all = "camelCase")] +/// struct CustomExtensions { +/// persisted_query: PersistedQueryExtensions, +/// } +/// #[derive(Deserialize, Serialize)] +/// #[serde(rename_all = "camelCase")] +/// struct PersistedQueryExtensions { +/// sha256_hash: Box, +/// } +/// +/// type CustomGraphQLRequest = GraphQLRequest; +/// +/// #[derive(Default)] +/// struct CustomContext { +/// persisted_query_sha256_hash: Option>, +/// } +/// impl Context for CustomContext {} +/// impl Inject for CustomContext { +/// fn inject(&mut self, extensions: &CustomExtensions) { +/// self.persisted_query_sha256_hash = Some(extensions.persisted_query.sha256_hash.clone()); +/// } +/// } +/// +/// struct Query; +/// +/// #[graphql_object] +/// impl Query { +/// fn is_persisted_query(context: &CustomContext) -> bool { +/// context.persisted_query_sha256_hash.is_some() +/// } +/// } +/// # +/// # type Schema = RootNode< +/// # Query, EmptyMutation, EmptySubscription, +/// # >; +/// # +/// # #[tokio::main] +/// # async fn main() { +/// # let request: CustomGraphQLRequest = serde_json::from_value(json!({ +/// # "query": "{ isPersistedQuery }", +/// # "extensions": { +/// # "persistedQuery": { +/// # "sha256Hash": +/// # "c205cf782b5c43c3fc67b5233445b78fbea47b99a0302cf31bda2a8e2162e1e6", +/// # }, +/// # }, +/// # })).unwrap(); +/// # let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()); +/// # let context = CustomContext::default(); +/// # +/// # assert_eq!( +/// # request.execute(&schema, context).await.into_result(), +/// # Ok((graphql_value!({"isPersistedQuery": true}), vec![])), +/// # ); +/// # } +/// ``` +/// +/// [0]: https://spec.graphql.org/September2025#sel-FANHLBBgBBvC0vW +pub trait Inject { + /// Injects the provided `value` into this [`Context`]. + /// + /// Default implementation does nothing. + fn inject(&mut self, value: &V) { + _ = value; + } +} + +impl Inject for () {} + /// Conversion trait for context types /// /// Used to support different context types for different parts of an @@ -394,99 +486,6 @@ pub trait FromContext { /// Perform the conversion fn from(value: &T) -> &Self; } - -/// Execution context for resolving GraphQL types. -pub trait Context { - /// Consumes the provided [request `extensions`][0] into this [`Context`]. - /// - /// # Implementation - /// - /// Default implementation does nothing. - /// - /// This method should be implemented if a [`Context`] implementation wants to be populated with - /// [request `extensions`][0]. Since [request `extensions`][0] could be modeled as an arbitrary - /// type, the implementation should downcast to the desired type before use. - /// - /// ```rust - /// # use juniper::{ - /// # Context, DefaultScalarValue, EmptyMutation, EmptySubscription, RootNode, - /// # graphql_object, graphql_value, http::GraphQLRequest, - /// # }; - /// # use serde::{Deserialize, Serialize}; - /// # use serde_json::json; - /// # - /// #[derive(Deserialize, Serialize)] - /// #[serde(rename_all = "camelCase")] - /// struct CustomExtensions { - /// persisted_query: PersistedQueryExtensions, - /// } - /// - /// #[derive(Deserialize, Serialize)] - /// #[serde(rename_all = "camelCase")] - /// struct PersistedQueryExtensions { - /// sha256_hash: Box, - /// } - /// - /// type CustomGraphQLRequest = GraphQLRequest; - /// - /// #[derive(Default)] - /// struct CustomContext { - /// persisted_query_sha256_hash: Option>, - /// } - /// - /// impl Context for CustomContext { - /// fn consume_request_extensions(&mut self, extensions: &T) { - /// use juniper::AnyExt as _; // allows downcasting directly on types without `dyn` - /// - /// if let Some(ext) = extensions.downcast_ref::() { - /// self.persisted_query_sha256_hash = - /// Some(ext.persisted_query.sha256_hash.clone()); - /// } - /// } - /// } - /// - /// struct Query; - /// - /// #[graphql_object] - /// impl Query { - /// fn is_persisted_query(context: &CustomContext) -> bool { - /// context.persisted_query_sha256_hash.is_some() - /// } - /// } - /// # - /// # type Schema = RootNode< - /// # Query, EmptyMutation, EmptySubscription, - /// # >; - /// # - /// # #[tokio::main] - /// # async fn main() { - /// # let request: CustomGraphQLRequest = serde_json::from_value(json!({ - /// # "query": "{ isPersistedQuery }", - /// # "extensions": { - /// # "persistedQuery": { - /// # "sha256Hash": - /// # "c205cf782b5c43c3fc67b5233445b78fbea47b99a0302cf31bda2a8e2162e1e6", - /// # }, - /// # }, - /// # })).unwrap(); - /// # let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()); - /// # let context = CustomContext::default(); - /// # - /// # assert_eq!( - /// # request.execute(&schema, context).await.into_result(), - /// # Ok((graphql_value!({"isPersistedQuery": true}), vec![])), - /// # ); - /// # } - /// ``` - /// - /// [0]: https://spec.graphql.org/September2025#sel-FANHLBBgBBvC0vW - fn consume_request_extensions(&mut self, extensions: &T) { - _ = extensions; - } -} - -impl Context for &C {} - static NULL_CONTEXT: () = (); impl FromContext for () { diff --git a/juniper/src/http/mod.rs b/juniper/src/http/mod.rs index 8836189a3..380725f35 100644 --- a/juniper/src/http/mod.rs +++ b/juniper/src/http/mod.rs @@ -9,10 +9,10 @@ use serde::{ }; use crate::{ - Context, FieldError, GraphQLError, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, - RootNode, Value, Variables, + FieldError, GraphQLError, GraphQLSubscriptionType, GraphQLType, GraphQLTypeAsync, RootNode, + Value, Variables, ast::InputValue, - executor::{ExecutionError, ValuesStream}, + executor::{ExecutionError, Inject, ValuesStream}, value::{DefaultScalarValue, ScalarValue}, }; @@ -77,16 +77,14 @@ where mut context: QueryT::Context, ) -> GraphQLResponse where - S: ScalarValue, - QueryT: GraphQLType, + QueryT: GraphQLType>, MutationT: GraphQLType, SubscriptionT: GraphQLType, - Ext: 'static, { let op = self.operation_name.as_deref(); let vars = &self.variables(); - if let Some(ext) = self.extensions.as_ref() { - context.consume_request_extensions(ext) + if let Some(extensions) = self.extensions.as_ref() { + context.inject(extensions); } let res = crate::execute_sync(&self.query, op, root_node, vars, &context); GraphQLResponse(res) @@ -102,16 +100,15 @@ where mut context: QueryT::Context, ) -> GraphQLResponse where - S: ScalarValue + Send + Sync, - QueryT: GraphQLTypeAsync, + S: Send + Sync, + QueryT: GraphQLTypeAsync + Sync>, MutationT: GraphQLTypeAsync, SubscriptionT: GraphQLType + Sync, - Ext: 'static, { let op = self.operation_name.as_deref(); let vars = &self.variables(); - if let Some(ext) = self.extensions.as_ref() { - context.consume_request_extensions(ext) + if let Some(extensions) = self.extensions.as_ref() { + context.inject(extensions); } let res = crate::execute(&self.query, op, root_node, vars, &context).await; GraphQLResponse(res) @@ -167,10 +164,9 @@ where context: QueryT::Context, ) -> GraphQLBatchResponse where - QueryT: GraphQLType, + QueryT: GraphQLType + Clone>, MutationT: GraphQLType, SubscriptionT: GraphQLType, - Ext: 'static, { match self { Self::Single(req) => GraphQLBatchResponse::Single(req.execute_sync(root_node, context)), @@ -193,10 +189,9 @@ where ) -> GraphQLBatchResponse where S: Send + Sync, - QueryT: GraphQLTypeAsync, + QueryT: GraphQLTypeAsync + Clone + Sync>, MutationT: GraphQLTypeAsync, SubscriptionT: GraphQLSubscriptionType, - Ext: 'static, { match self { Self::Single(req) => { diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index ff83576d2..5dfed3692 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -89,7 +89,7 @@ pub use crate::{ Selection, ToInputValue, Type, }, executor::{ - Applies, Context, ExecutionError, ExecutionResult, Executor, FieldError, FieldResult, + Applies, Context, Inject, ExecutionError, ExecutionResult, Executor, FieldError, FieldResult, FromContext, IntoFieldError, IntoResolvable, LookAheadArgument, LookAheadChildren, LookAheadList, LookAheadObject, LookAheadSelection, LookAheadValue, OwnedExecutor, Registry, ValuesStream, Variables, From ad812e9ec65eba95e8a54e44f381dec7220bcb27 Mon Sep 17 00:00:00 2001 From: tyranron Date: Tue, 28 Oct 2025 17:53:14 +0200 Subject: [PATCH 7/7] Switch `juniper_actix` [skip ci] --- juniper/src/executor/mod.rs | 4 +-- juniper/src/tests/fixtures/starwars/schema.rs | 4 ++- juniper_actix/src/lib.rs | 30 +++++++++---------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/juniper/src/executor/mod.rs b/juniper/src/executor/mod.rs index 9ca755c30..8554b6707 100644 --- a/juniper/src/executor/mod.rs +++ b/juniper/src/executor/mod.rs @@ -460,7 +460,7 @@ impl Context for &C {} /// ``` /// /// [0]: https://spec.graphql.org/September2025#sel-FANHLBBgBBvC0vW -pub trait Inject { +pub trait Inject { /// Injects the provided `value` into this [`Context`]. /// /// Default implementation does nothing. @@ -469,7 +469,7 @@ pub trait Inject { } } -impl Inject for () {} +impl Inject for () {} /// Conversion trait for context types /// diff --git a/juniper/src/tests/fixtures/starwars/schema.rs b/juniper/src/tests/fixtures/starwars/schema.rs index f16940863..897e69f8f 100644 --- a/juniper/src/tests/fixtures/starwars/schema.rs +++ b/juniper/src/tests/fixtures/starwars/schema.rs @@ -9,7 +9,7 @@ use std::{collections::HashMap, pin::Pin}; -use crate::{Context, GraphQLEnum, graphql_interface, graphql_object, graphql_subscription}; +use crate::{Context, Inject, GraphQLEnum, graphql_interface, graphql_object, graphql_subscription}; #[derive(Clone, Copy, Debug)] pub struct Query; @@ -209,6 +209,8 @@ pub struct Database { impl Context for Database {} +impl Inject for Database {} + impl Database { pub fn new() -> Database { let mut humans = HashMap::new(); diff --git a/juniper_actix/src/lib.rs b/juniper_actix/src/lib.rs index cb4bb146f..a2794a82e 100644 --- a/juniper_actix/src/lib.rs +++ b/juniper_actix/src/lib.rs @@ -10,11 +10,12 @@ use actix_web::{ http::Method, web, }; use juniper::{ - Context, DefaultScalarValue, GraphQLSubscriptionType, GraphQLTypeAsync, ScalarValue, + Inject, DefaultScalarValue, GraphQLSubscriptionType, GraphQLTypeAsync, ScalarValue, http::{ GraphQLBatchRequest, GraphQLRequest, graphiql::graphiql_source, playground::playground_source, }, + Variables }; use serde::{Deserialize, de::DeserializeOwned}; @@ -24,10 +25,10 @@ pub struct GraphQLWith, ); -impl GraphQLWith +impl GraphQLWith where S: ScalarValue + Send + Sync, - RequestExtensions: DeserializeOwned + 'static, + Extensions: DeserializeOwned, { pub async fn handler( schema: &juniper::RootNode, @@ -36,14 +37,13 @@ where payload: web::Payload, ) -> Result where - Query: GraphQLTypeAsync, + Query: GraphQLTypeAsync + Clone + Sync>, Mutation: GraphQLTypeAsync, Subscription: GraphQLSubscriptionType, - S: ScalarValue + Send + Sync, { match *req.method() { - Method::POST => post_graphql_handler(schema, context, req, payload).await, - Method::GET => get_graphql_handler(schema, context, req).await, + Method::POST => Self::post_handler(schema, context, req, payload).await, + Method::GET => Self::get_handler(schema, context, req).await, _ => Err(actix_web::error::UrlGenerationError::ResourceNotFound.into()), } } @@ -54,12 +54,12 @@ where req: HttpRequest, ) -> Result where - Query: GraphQLTypeAsync, + Query: GraphQLTypeAsync + Sync>, Mutation: GraphQLTypeAsync, Subscription: GraphQLSubscriptionType, { let get_req = web::Query::::from_query(req.query_string())?; - let req = GraphQLRequest::::try_from(get_req.into_inner())?; + let req = GraphQLRequest::::try_from(get_req.into_inner())?; let gql_response = req.execute(schema, context).await; let body_response = serde_json::to_string(&gql_response)?; let mut response = match gql_response.is_ok() { @@ -78,19 +78,19 @@ where payload: web::Payload, ) -> Result where - Query: GraphQLTypeAsync, + Query: GraphQLTypeAsync + Clone + Sync>, Mutation: GraphQLTypeAsync, Subscription: GraphQLSubscriptionType, { let req = match req.content_type() { "application/json" => { let body = String::from_request(&req, &mut payload.into_inner()).await?; - serde_json::from_str::>(&body) + serde_json::from_str::>(&body) .map_err(JsonPayloadError::Deserialize) } "application/graphql" => { let body = String::from_request(&req, &mut payload.into_inner()).await?; - Ok(GraphQLBatchRequest::::Single( + Ok(GraphQLBatchRequest::::Single( GraphQLRequest { query: body, operation_name: None, @@ -161,7 +161,7 @@ pub async fn graphql_handler( payload: web::Payload, ) -> Result where - Query: GraphQLTypeAsync, + Query: GraphQLTypeAsync> + Clone + Sync>, Mutation: GraphQLTypeAsync, Subscription: GraphQLSubscriptionType, S: ScalarValue + Send + Sync, @@ -176,7 +176,7 @@ pub async fn get_graphql_handler( req: HttpRequest, ) -> Result where - Query: GraphQLTypeAsync, + Query: GraphQLTypeAsync> + Sync>, Mutation: GraphQLTypeAsync, Subscription: GraphQLSubscriptionType, S: ScalarValue + Send + Sync, @@ -192,7 +192,7 @@ pub async fn post_graphql_handler( payload: web::Payload, ) -> Result where - Query: GraphQLTypeAsync, + Query: GraphQLTypeAsync> + Clone + Sync>, Mutation: GraphQLTypeAsync, Subscription: GraphQLSubscriptionType, S: ScalarValue + Send + Sync,