Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions juniper/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ 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])
- `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

Expand Down Expand Up @@ -72,12 +77,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
Expand Down
252 changes: 119 additions & 133 deletions juniper/src/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ 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<S = DefaultScalarValue>
// Should be specified as top-level, otherwise `serde` infers incorrect `Ext: Default` bound.
#[serde(bound(deserialize = "Ext: Deserialize<'de>"))]
pub struct GraphQLRequest<S = DefaultScalarValue, Ext = Variables<S>>
where
S: ScalarValue,
{
Expand All @@ -42,19 +44,18 @@ where
serialize = "InputValue<S>: Serialize",
))]
pub variables: Option<InputValue<S>>,

/// 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<Ext>,
}

impl<S> GraphQLRequest<S>
impl<S, Ext> GraphQLRequest<S, Ext>
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<S> {
self.variables
Expand All @@ -66,19 +67,6 @@ where
.unwrap_or_default()
}

/// Construct a new GraphQL request from parts
pub fn new(
query: String,
operation_name: Option<String>,
variables: Option<InputValue<S>>,
) -> Self {
Self {
query,
operation_name,
variables,
}
}

/// Execute a GraphQL request synchronously using the specified schema and context
///
/// This is a simple wrapper around the `execute_sync` function exposed at the
Expand Down Expand Up @@ -129,6 +117,112 @@ where
}
}

/// Simple wrapper around GraphQLRequest to allow the handling of Batch requests.
#[derive(Debug, Deserialize, PartialEq)]
#[serde(untagged)]
#[serde(bound = "GraphQLRequest<S, Ext>: Deserialize<'de>")]
pub enum GraphQLBatchRequest<S = DefaultScalarValue, Ext = Variables<S>>
where
S: ScalarValue,
{
/// A single operation request.
Single(GraphQLRequest<S, Ext>),

/// A batch operation request.
///
/// Empty batch is considered as invalid value, so cannot be deserialized.
#[serde(deserialize_with = "deserialize_non_empty_batch")]
Batch(Vec<GraphQLRequest<S, Ext>>),
}

fn deserialize_non_empty_batch<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
where
D: de::Deserializer<'de>,
T: Deserialize<'de>,
{
use de::Error as _;

let v = Vec::<T>::deserialize(deserializer)?;
if v.is_empty() {
Err(D::Error::invalid_length(
0,
&"non-empty batch of GraphQL requests",
))
} else {
Ok(v)
}
}

impl<S, Ext> GraphQLBatchRequest<S, Ext>
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<QueryT, MutationT, SubscriptionT, S>,
context: &QueryT::Context,
) -> GraphQLBatchResponse<S>
where
QueryT: GraphQLType<S>,
MutationT: GraphQLType<S, Context = QueryT::Context>,
SubscriptionT: GraphQLType<S, Context = QueryT::Context>,
{
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<QueryT, MutationT, SubscriptionT, S>,
context: &'a QueryT::Context,
) -> GraphQLBatchResponse<S>
where
QueryT: GraphQLTypeAsync<S>,
QueryT::TypeInfo: Sync,
QueryT::Context: Sync,
MutationT: GraphQLTypeAsync<S, Context = QueryT::Context>,
MutationT::TypeInfo: Sync,
SubscriptionT: GraphQLSubscriptionType<S, Context = QueryT::Context>,
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<Option<&str>> {
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<ValuesStream<S>` using the
/// specified schema and context.
/// This is a wrapper around the `resolve_into_stream` function exposed at the top
Expand Down Expand Up @@ -208,8 +302,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")?;
Expand All @@ -222,7 +316,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)?;
Expand All @@ -232,114 +326,6 @@ where
}
}

/// Simple wrapper around GraphQLRequest to allow the handling of Batch requests.
#[derive(Debug, Deserialize, PartialEq)]
#[serde(untagged)]
#[serde(bound = "InputValue<S>: Deserialize<'de>")]
pub enum GraphQLBatchRequest<S = DefaultScalarValue>
where
S: ScalarValue,
{
/// A single operation request.
Single(GraphQLRequest<S>),

/// A batch operation request.
///
/// Empty batch is considered as invalid value, so cannot be deserialized.
#[serde(deserialize_with = "deserialize_non_empty_batch")]
Batch(Vec<GraphQLRequest<S>>),
}

fn deserialize_non_empty_batch<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
where
D: de::Deserializer<'de>,
T: Deserialize<'de>,
{
use de::Error as _;

let v = Vec::<T>::deserialize(deserializer)?;
if v.is_empty() {
Err(D::Error::invalid_length(
0,
&"non-empty batch of GraphQL requests",
))
} else {
Ok(v)
}
}

impl<S> GraphQLBatchRequest<S>
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<QueryT, MutationT, SubscriptionT, S>,
context: &QueryT::Context,
) -> GraphQLBatchResponse<S>
where
QueryT: GraphQLType<S>,
MutationT: GraphQLType<S, Context = QueryT::Context>,
SubscriptionT: GraphQLType<S, Context = QueryT::Context>,
{
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<QueryT, MutationT, SubscriptionT, S>,
context: &'a QueryT::Context,
) -> GraphQLBatchResponse<S>
where
QueryT: GraphQLTypeAsync<S>,
QueryT::TypeInfo: Sync,
QueryT::Context: Sync,
MutationT: GraphQLTypeAsync<S, Context = QueryT::Context>,
MutationT::TypeInfo: Sync,
SubscriptionT: GraphQLSubscriptionType<S, Context = QueryT::Context>,
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<Option<&str>> {
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
Expand Down
7 changes: 6 additions & 1 deletion juniper/src/tests/subscriptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,12 @@ fn create_and_execute(
),
Vec<ExecutionError<DefaultScalarValue>>,
> {
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);

Expand Down
16 changes: 12 additions & 4 deletions juniper_actix/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
}

Expand Down Expand Up @@ -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),
}?;
Expand Down
Loading