-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(graphql): add graphql-over-http client (#9)
- Loading branch information
Showing
17 changed files
with
2,453 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,4 +3,5 @@ resolver = "2" | |
members = [ | ||
"toolshed", | ||
"graphql", | ||
"graphql-http" | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
[package] | ||
name = "graphql-http" | ||
description = "A reqwest based GraphQL-over-HTTP client" | ||
version = "0.1.0" | ||
edition = "2021" | ||
rust-version = "1.63.0" | ||
|
||
[features] | ||
http-reqwest = ["dep:async-trait", "dep:thiserror", "dep:reqwest"] | ||
compat-graphql-client = ["dep:graphql_client"] | ||
compat-graphql-parser = ["dep:graphql-parser"] | ||
|
||
[dependencies] | ||
anyhow = "1.0.75" | ||
async-trait = { version = "0.1", optional = true } | ||
graphql-parser = { version = "0.4.0", optional = true } | ||
graphql_client = { version = "0.13.0", optional = true } | ||
reqwest = { version = "0.11", optional = true } | ||
serde = { version = "1.0", features = ["derive"] } | ||
serde_json = "1.0" | ||
thiserror = { version = "1.0", optional = true } | ||
|
||
[dev-dependencies] | ||
assert_matches = "1.5.0" | ||
indoc = "2.0.4" | ||
tokio = { version = "1.32", features = ["rt", "macros", "time"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
GraphQL-over-HTTP | ||
================= | ||
|
||
A _reqwest_ based GraphQL-over-HTTP client compatible with `graphql-parser` and `graphql-client` crates. See the | ||
crate documentation for more details. | ||
|
||
``` | ||
graphql-http = { git = "https://github.com/edgeandnode/toolshed", tag = "graphql-http-vX.Y.Z" } | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
#[cfg(feature = "compat-graphql-parser")] | ||
pub mod compat_graphql_parser { | ||
use graphql_parser::query::Text; | ||
|
||
use crate::graphql::{Document, IntoDocument}; | ||
|
||
// Implement `IntoRequestParameters` for `graphql_parser::query::Document` so that we can use | ||
// `graphql_parser` to parse GraphQL queries. | ||
// | ||
// As any type implementing `IntoQuery` also implements `IntoRequestParameters`, this allows us to | ||
// seamlessly support `graphql_parser` generated queries. | ||
impl<'a, T: Text<'a>> IntoDocument for graphql_parser::query::Document<'a, T> { | ||
fn into_document(self) -> Document { | ||
Document::new(self.to_string()) | ||
} | ||
} | ||
} | ||
|
||
#[cfg(feature = "compat-graphql-client")] | ||
pub mod compat_graphql_client { | ||
use graphql_client::QueryBody; | ||
|
||
use crate::graphql::IntoDocument; | ||
use crate::http::request::{IntoRequestParameters, RequestParameters}; | ||
|
||
// Implement `IntoRequestParameters` for `graphql_client::QueryBody` so that we can seamlessly | ||
// support `graphql_client` generated queries. | ||
impl<V> IntoRequestParameters for QueryBody<V> | ||
where | ||
V: serde::ser::Serialize, | ||
{ | ||
fn into_request_parameters(self) -> RequestParameters { | ||
let query = self.query.into_document(); | ||
|
||
// Do not send the `operation_name` field if it is empty. | ||
let operation_name = if !self.operation_name.is_empty() { | ||
Some(self.operation_name.to_owned()) | ||
} else { | ||
None | ||
}; | ||
|
||
// Do not send the `variables` field if the json serialization fails, or if the | ||
// serialization result is not a JSON object. | ||
let variables = match serde_json::to_value(self.variables) { | ||
Ok(serde_json::Value::Object(vars)) => Some(vars), | ||
_ => None, | ||
}; | ||
|
||
RequestParameters { | ||
query, | ||
operation_name, | ||
variables: variables.unwrap_or_default(), | ||
extensions: Default::default(), | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
//! GraphQL query type and related traits. | ||
//! | ||
//! This module contains the [`Document`] type and the [`IntoDocument`] conversion trait. The conversion | ||
//! trait is implemented for string types: [`String`] and `&str`. | ||
//! | ||
|
||
/// A (raw) GraphQL request document. | ||
/// | ||
/// This type is a wrapper around a string that represents a GraphQL request document. This type | ||
/// does not perform any validation on the string. | ||
#[derive(Debug, Clone, PartialEq, Eq)] | ||
pub struct Document(String); | ||
|
||
impl Document { | ||
/// Create a new GraphQL [`Document`] instance from a `String`. | ||
pub fn new(value: String) -> Self { | ||
Self(value) | ||
} | ||
|
||
/// Return a string slice to the document. | ||
pub fn as_str(&self) -> &str { | ||
&self.0 | ||
} | ||
} | ||
|
||
impl From<String> for Document { | ||
fn from(value: String) -> Self { | ||
Self::new(value) | ||
} | ||
} | ||
|
||
impl std::fmt::Display for Document { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
self.0.fmt(f) | ||
} | ||
} | ||
|
||
impl serde::ser::Serialize for Document { | ||
fn serialize<S: serde::ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { | ||
self.0.serialize(serializer) | ||
} | ||
} | ||
|
||
/// A trait for types that can be converted into a [`Document`]. | ||
pub trait IntoDocument { | ||
/// Consumes `self` and returns a [`Document`]. | ||
fn into_document(self) -> Document; | ||
} | ||
|
||
/// A trait for types that can be converted into a [`Document`] and variables tuple. | ||
pub trait IntoDocumentWithVariables { | ||
type Variables: serde::Serialize; | ||
|
||
/// Consumes `self` and returns a query and variables tuple. | ||
fn into_document_with_variables(self) -> (Document, Self::Variables); | ||
} | ||
|
||
impl IntoDocument for Document { | ||
fn into_document(self) -> Document { | ||
self | ||
} | ||
} | ||
|
||
impl IntoDocument for String { | ||
fn into_document(self) -> Document { | ||
Document(self) | ||
} | ||
} | ||
|
||
impl IntoDocument for &str { | ||
fn into_document(self) -> Document { | ||
Document(self.to_owned()) | ||
} | ||
} | ||
|
||
impl<T> IntoDocumentWithVariables for T | ||
where | ||
T: IntoDocument, | ||
{ | ||
type Variables = (); | ||
|
||
fn into_document_with_variables(self) -> (Document, Self::Variables) { | ||
(self.into_document(), ()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
pub mod request; | ||
pub mod response; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
use crate::graphql::{Document, IntoDocumentWithVariables}; | ||
|
||
/// The media type for GraphQL-over-HTTP requests. As specified in the section | ||
/// [4.1 Media Types](https://graphql.github.io/graphql-over-http/draft/#sec-Media-Types) | ||
/// of the GraphQL-over-HTTP specification. | ||
pub const GRAPHQL_REQUEST_MEDIA_TYPE: &str = "application/json"; | ||
|
||
/// The parameters of a GraphQL-over-HTTP request. | ||
/// | ||
/// As specified in the section [5.1 Request Parameters](https://graphql.github.io/graphql-over-http/draft/#sec-Request-Parameters) | ||
/// of the GraphQL-over-HTTP specification. | ||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] | ||
pub struct RequestParameters { | ||
/// The string representation of the Source Text of a GraphQL Document as specified in [the | ||
/// Language section of the GraphQL specification](https://spec.graphql.org/draft/#sec-Language). | ||
pub query: Document, | ||
|
||
/// Optional name of the operation in the Document to execute. | ||
#[serde(rename = "operationName", skip_serializing_if = "Option::is_none")] | ||
pub operation_name: Option<String>, | ||
|
||
/// Values for any Variables defined by the Operation. | ||
#[serde(skip_serializing_if = "serde_json::Map::is_empty")] | ||
pub variables: serde_json::Map<String, serde_json::Value>, | ||
|
||
/// Reserved for implementors to extend the protocol however they see fit. | ||
#[serde(skip_serializing_if = "serde_json::Map::is_empty")] | ||
pub extensions: serde_json::Map<String, serde_json::Value>, | ||
} | ||
|
||
/// Convert `self` into a `RequestParameters` struct. | ||
pub trait IntoRequestParameters { | ||
/// Consumes `self` and returns a `RequestParameters` struct. | ||
fn into_request_parameters(self) -> RequestParameters; | ||
} | ||
|
||
impl IntoRequestParameters for RequestParameters { | ||
fn into_request_parameters(self) -> RequestParameters { | ||
self | ||
} | ||
} | ||
|
||
// Any type implementing `IntoQueryWithVariables` (or `IntoQuery`) can be converted into | ||
// `RequestParameters`. | ||
impl<T> IntoRequestParameters for T | ||
where | ||
T: IntoDocumentWithVariables, | ||
{ | ||
fn into_request_parameters(self) -> RequestParameters { | ||
let (query, variables) = self.into_document_with_variables(); | ||
|
||
// Do not send the `variables` field if the json serialization fails, or if the | ||
// serialization result is not a JSON object. | ||
let variables = match serde_json::to_value(variables) { | ||
Ok(serde_json::Value::Object(vars)) => Some(vars), | ||
_ => None, | ||
}; | ||
|
||
RequestParameters { | ||
query, | ||
operation_name: None, | ||
variables: variables.unwrap_or_default(), | ||
extensions: Default::default(), | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
/// The preferred type for GraphQL-over-HTTP server responses. As specified in the section | ||
/// [4.1 Media Types](https://graphql.github.io/graphql-over-http/draft/#sec-Media-Types) of the | ||
/// GraphQL-over-HTTP specification. | ||
pub const GRAPHQL_RESPONSE_MEDIA_TYPE: &str = "application/graphql-response+json"; | ||
|
||
/// The legacy type for GraphQL-over-HTTP server responses. As specified in the section | ||
/// [4.1 Media Types](https://graphql.github.io/graphql-over-http/draft/#sec-Media-Types) of the | ||
/// GraphQL-over-HTTP specification. | ||
pub const GRAPHQL_LEGACY_RESPONSE_MEDIA_TYPE: &str = "application/json"; | ||
|
||
/// The response error type for GraphQL-over-HTTP server responses. As specified in the section | ||
/// [7.1.2 Errors](https://spec.graphql.org/draft/#sec-Errors) and the | ||
/// [Error Result Format](https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format) subsection | ||
/// of the GraphQL specification. | ||
#[derive(Debug, serde::Deserialize)] | ||
pub struct Error { | ||
/// A short, human-readable description of the problem. | ||
/// | ||
/// From the [Error Result Format](https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format) | ||
/// subsection of the GraphQL specification: | ||
/// | ||
/// > Every error MUST contain an entry with the key `message` with a string description of the | ||
/// > error intended for the developer as a guide to understand and correct the error. | ||
pub message: String, | ||
|
||
/// A list of locations describing the beginning of the associated syntax element causing the | ||
/// error. | ||
/// | ||
/// From the [Error Result Format](https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format) | ||
/// subsection of the GraphQL specification: | ||
/// | ||
/// > If an error can be associated to a particular point in the requested GraphQL document, it | ||
/// > SHOULD contain an entry with the key `locations` with a list of locations, where each | ||
/// > location is a map with the keys `line` and `column`, both positive numbers starting from | ||
/// `1` which describe the beginning of an associated syntax element. | ||
#[serde(default)] | ||
pub locations: Vec<ErrorLocation>, | ||
|
||
/// A list of path segments starting at the root of the response and ending with the field | ||
/// associated with the error. | ||
/// | ||
/// From the [Error Result Format](https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format) | ||
/// subsection of the GraphQL specification: | ||
/// | ||
/// > If an error can be associated to a particular field in the GraphQL result, it must contain | ||
/// > an entry with the key `path` that details the path of the response field which experienced | ||
/// > the error. This allows clients to identify whether a `null` result is intentional or | ||
/// > caused by a runtime error. | ||
/// > | ||
/// > This field should be a list of path segments starting at the root of the response and | ||
/// > ending with the field associated with the error. Path segments that represent fields | ||
/// > should be strings, and path segments that represent list indices should be 0-indexed | ||
/// > integers. If the error happens in an aliased field, the path to the error should use the | ||
/// > aliased name, since it represents a path in the response, not in the request. | ||
#[serde(default)] | ||
pub path: Vec<String>, | ||
} | ||
|
||
/// A location describing the beginning of the associated syntax element causing the error. | ||
#[derive(Debug, serde::Deserialize)] | ||
pub struct ErrorLocation { | ||
pub line: usize, | ||
pub column: usize, | ||
} | ||
|
||
/// A response to a GraphQL request. | ||
/// | ||
/// As specified in the section [7. Response](https://spec.graphql.org/draft/#sec-Response) of the | ||
/// GraphQL specification. | ||
#[derive(Debug, serde::Deserialize)] | ||
pub struct ResponseBody<T> { | ||
/// The e response will be the result of the execution of the requested operation. | ||
/// | ||
/// If the operation was a query, this output will be an object of the query root operation | ||
/// type; if the operation was a mutation, this output will be an object of the mutation root | ||
/// operation type. | ||
/// | ||
/// If an error was raised before execution begins, the data entry should not be present in the | ||
/// result; If an error was raised during the execution that prevented a valid response, the | ||
/// data entry in the response should be `null`. In both cases the field will be set to `None`. | ||
pub data: Option<T>, | ||
|
||
/// The errors entry in the response is a non-empty list of [`Error`] raised during the request, | ||
/// where each error is a map of data described by the error result specified in the section | ||
/// [7.1.2. Errors](https://spec.graphql.org/draft/#sec-Errors) of the GraphQL specification. | ||
#[serde(default)] | ||
pub errors: Vec<Error>, | ||
} |
Oops, something went wrong.