Skip to content
Open
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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html

## Unreleased - xxxx-xx-xx

### Breaking Changes

- Removed the `ErrorExtensions` generic parameter from `GraphQlResponse`
- `GraphQlResponse::new` no longer accepts error extensions - use the new
`set_extensions` or `with_extensions` functions to set these
- The `extensions` field of `GraphQlError` is now private.
- Removed `CynicReqwestBuilder::retain_extensions` - users should use the
`GraphQlError::extensions` instead.

### New Features

- Added new methods for deserializing extensions in responses:
- `GraphQlResponse::extensions` for deserializing response extensions.
- `GraphQlError::extensions` for reading error extensions

## v3.10.0 - 2025-02-10

### New Features
Expand Down
32 changes: 7 additions & 25 deletions cynic/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ pub enum CynicReqwestError {
#[cfg(feature = "http-reqwest")]
mod reqwest_ext {
use super::CynicReqwestError;
use std::{future::Future, marker::PhantomData, pin::Pin};
use std::{future::Future, pin::Pin};

use crate::{GraphQlResponse, Operation};

Expand Down Expand Up @@ -219,13 +219,13 @@ mod reqwest_ext {
}
}

impl<ResponseData: serde::de::DeserializeOwned, Errors: serde::de::DeserializeOwned>
std::future::IntoFuture for CynicReqwestBuilder<ResponseData, Errors>
impl<ResponseData: serde::de::DeserializeOwned> std::future::IntoFuture
for CynicReqwestBuilder<ResponseData>
{
type Output = Result<GraphQlResponse<ResponseData, Errors>, CynicReqwestError>;
type Output = Result<GraphQlResponse<ResponseData>, CynicReqwestError>;

type IntoFuture =
BoxFuture<'static, Result<GraphQlResponse<ResponseData, Errors>, CynicReqwestError>>;
BoxFuture<'static, Result<GraphQlResponse<ResponseData>, CynicReqwestError>>;

fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
Expand All @@ -235,29 +235,11 @@ mod reqwest_ext {
}
}

impl<ResponseData> CynicReqwestBuilder<ResponseData, serde::de::IgnoredAny> {
/// Sets the type that will be deserialized for the extensions fields of any errors in the response
pub fn retain_extensions<ErrorExtensions>(
self,
) -> CynicReqwestBuilder<ResponseData, ErrorExtensions>
where
ErrorExtensions: serde::de::DeserializeOwned,
{
let CynicReqwestBuilder { builder, _marker } = self;

CynicReqwestBuilder {
builder,
_marker: PhantomData,
}
}
}

async fn deser_gql<ResponseData, ErrorExtensions>(
async fn deser_gql<ResponseData>(
response: Result<reqwest::Response, reqwest::Error>,
) -> Result<GraphQlResponse<ResponseData, ErrorExtensions>, CynicReqwestError>
) -> Result<GraphQlResponse<ResponseData>, CynicReqwestError>
where
ResponseData: serde::de::DeserializeOwned,
ErrorExtensions: serde::de::DeserializeOwned,
{
let response = match response {
Ok(response) => response,
Expand Down
4 changes: 2 additions & 2 deletions cynic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ mod builders;
mod core;
mod id;
mod operation;
mod result;
mod response;

pub mod coercions;
pub mod queries;
Expand All @@ -190,7 +190,7 @@ pub use {
builders::{MutationBuilder, QueryBuilder, SubscriptionBuilder},
id::Id,
operation::{Operation, OperationBuildError, OperationBuilder, StreamingOperation},
result::*,
response::*,
variables::{QueryVariableLiterals, QueryVariables, QueryVariablesFields},
};

Expand Down
81 changes: 68 additions & 13 deletions cynic/src/result.rs → cynic/src/response.rs
Original file line number Diff line number Diff line change
@@ -1,42 +1,88 @@
/// The response to a GraphQl operation
#[derive(Debug, Clone)]
pub struct GraphQlResponse<T, ErrorExtensions = serde::de::IgnoredAny> {
pub struct GraphQlResponse<T> {
/// The operation data (if the operation was successful)
pub data: Option<T>,

/// Any errors that occurred as part of this operation
pub errors: Option<Vec<GraphQlError<ErrorExtensions>>>,
pub errors: Option<Vec<GraphQlError>>,

/// Optional arbitrary extra data describing the error in more detail.
extensions: Option<serde_json::Value>,
}

impl<T> GraphQlResponse<T> {
/// Deserialize the extensions field on this response as an instance of E
pub fn extensions<'a, E>(&'a self) -> Result<E, serde_json::Error>
where
E: serde::Deserialize<'a>,
{
let Some(extensions) = self.extensions.as_ref() else {
return E::deserialize(serde_json::Value::Null);
};

E::deserialize(extensions)
}
}

/// A model describing an error which has taken place during execution.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, thiserror::Error)]
#[error("{message}")]
pub struct GraphQlError<Extensions = serde::de::IgnoredAny> {
pub struct GraphQlError {
/// A description of the error which has taken place.
pub message: String,
/// Optional description of the locations where the errors have taken place.
pub locations: Option<Vec<GraphQlErrorLocation>>,
/// Optional path to the response field which experienced the associated error.
pub path: Option<Vec<GraphQlErrorPathSegment>>,
/// Optional arbitrary extra data describing the error in more detail.
pub extensions: Option<Extensions>,
extensions: Option<serde_json::Value>,
}

impl<ErrorExtensions> GraphQlError<ErrorExtensions> {
impl GraphQlError {
/// Construct a new instance.
pub fn new(
message: String,
locations: Option<Vec<GraphQlErrorLocation>>,
path: Option<Vec<GraphQlErrorPathSegment>>,
extensions: Option<ErrorExtensions>,
) -> Self {
GraphQlError {
message,
locations,
path,
extensions,
extensions: None,
}
}

/// Populate the extensions field of this error
pub fn with_extensions<E>(mut self, extensions: E) -> Result<Self, serde_json::Error>
where
E: serde::Serialize,
{
self.set_extensions(extensions)?;
Ok(self)
}

/// Populate the extensions field of this error
pub fn set_extensions<E>(&mut self, extensions: E) -> Result<(), serde_json::Error>
where
E: serde::Serialize,
{
self.extensions = Some(serde_json::to_value(extensions)?);
Ok(())
}

/// Deserialize the extensions field on this error as an instance of E
pub fn extensions<'a, E>(&'a self) -> Result<E, serde_json::Error>
where
E: serde::Deserialize<'a>,
{
let Some(extensions) = self.extensions.as_ref() else {
return E::deserialize(serde_json::Value::Null);
};

E::deserialize(extensions)
}
}

/// A line and column offset describing the location of an error within a GraphQL document.
Expand All @@ -58,10 +104,9 @@ pub enum GraphQlErrorPathSegment {
Index(i32),
}

impl<'de, T, ErrorExtensions> serde::Deserialize<'de> for GraphQlResponse<T, ErrorExtensions>
impl<'de, T> serde::Deserialize<'de> for GraphQlResponse<T>
where
T: serde::Deserialize<'de>,
ErrorExtensions: serde::Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
Expand All @@ -70,23 +115,33 @@ where
use serde::de::Error;

#[derive(serde::Deserialize)]
struct ResponseDeser<T, ErrorExtensions> {
struct ResponseDeser<T> {
/// The operation data (if the operation was successful)
data: Option<T>,

/// Any errors that occurred as part of this operation
errors: Option<Vec<GraphQlError<ErrorExtensions>>>,
errors: Option<Vec<GraphQlError>>,

extensions: Option<serde_json::Value>,
}

let ResponseDeser { data, errors } = ResponseDeser::deserialize(deserializer)?;
let ResponseDeser {
data,
errors,
extensions,
} = ResponseDeser::deserialize(deserializer)?;

if data.is_none() && errors.is_none() {
return Err(D::Error::custom(
"Either data or errors must be present in a GraphQL response",
));
}

Ok(GraphQlResponse { data, errors })
Ok(GraphQlResponse {
data,
errors,
extensions,
})
}
}

Expand Down
14 changes: 10 additions & 4 deletions cynic/tests/http.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use cynic::QueryBuilder;
use serde_json::json;

mod schema {
cynic::use_schema!("tests/test-schema.graphql");
Expand All @@ -17,7 +18,7 @@ pub struct FieldWithString {
pub field_with_string: i32,
}

#[derive(serde::Deserialize)]
#[derive(serde::Deserialize, PartialEq, Debug)]
struct Extensions {
code: u16,
}
Expand Down Expand Up @@ -54,7 +55,6 @@ async fn test_reqwest_extensions() {
.run_graphql(FieldWithString::build(FieldWithStringVariables {
input: "InputGoesHere",
}))
.retain_extensions::<Extensions>()
.await;
assert!(output.is_ok());

Expand All @@ -64,7 +64,10 @@ async fn test_reqwest_extensions() {
let errors = err.errors.unwrap();

let error = &errors[0];
assert!(matches!(error.extensions, Some(Extensions { code: 401 })));
assert_eq!(
error.extensions::<Extensions>().unwrap(),
Extensions { code: 401 }
);

response_with_extension.assert();
}
Expand Down Expand Up @@ -110,7 +113,10 @@ async fn test_reqwest_ignored() {
let errors = err.errors.unwrap();

let error = &errors[0];
assert!(matches!(error.extensions, Some(serde::de::IgnoredAny)));
assert_eq!(
error.extensions::<serde_json::Value>().unwrap(),
json!({"code": 401})
);

response_with_extension.assert();
}
Loading