Skip to content

Commit

Permalink
New features (#24)
Browse files Browse the repository at this point in the history
* CI

* Add target

* Clippy

* Update client config, improve #22

* Improve CI

* Support proxy, fix #23

* fix tests

* fix darwinia-network/bridger#536

* format and bump version

* fix ci
  • Loading branch information
fewensa authored Oct 28, 2022
1 parent 6860fd0 commit 4b8189f
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 105 deletions.
19 changes: 0 additions & 19 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ jobs:
steps:
- uses: actions/checkout@v2

- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true

- name: Clippy
run: cargo clippy --all -- -D warnings

Expand All @@ -27,12 +21,6 @@ jobs:
steps:
- uses: actions/checkout@v2

- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true

- name: Test
run: cargo test

Expand All @@ -42,13 +30,6 @@ jobs:
steps:
- uses: actions/checkout@v2

- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
override: true

- name: Check wasm32
run: cargo check --target wasm32-unknown-unknown

Expand Down
6 changes: 0 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@ jobs:
steps:
- uses: actions/checkout@v2

- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true

- name: Package
run: cargo package

Expand Down
20 changes: 10 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
[package]
name = "gql_client"
version = "1.0.6"
authors = ["Arthur Khlghatyan <[email protected]>"]
edition = "2018"
name = "gql_client"
version = "1.0.7"
authors = ["Arthur Khlghatyan <[email protected]>"]
edition = "2018"
description = "Minimal GraphQL client for Rust"
readme = "README.md"
homepage = "https://github.com/arthurkhlghatyan/gql-client-rs"
repository = "https://github.com/arthurkhlghatyan/gql-client-rs"
license = "MIT"
keywords = ["graphql", "client", "async", "web", "http"]
categories = ["web-programming", "asynchronous"]
readme = "README.md"
homepage = "https://github.com/arthurkhlghatyan/gql-client-rs"
repository = "https://github.com/arthurkhlghatyan/gql-client-rs"
license = "MIT"
keywords = ["graphql", "client", "async", "web", "http"]
categories = ["web-programming", "asynchronous"]

[badges]
maintenance = { status = "actively-developed" }
Expand Down
5 changes: 5 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[toolchain]
channel = "stable"
components = ["cargo", "clippy", "rustc", "rustfmt", "rust-src"]
profile = "minimal"
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
203 changes: 139 additions & 64 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
use std::collections::HashMap;
#[cfg(not(target_arch = "wasm32"))]
use std::convert::TryInto;
use std::str::FromStr;

use reqwest::{
header::{HeaderMap, HeaderName, HeaderValue},
Client,
};
use reqwest::{Client, Url};
use serde::{Deserialize, Serialize};

use crate::error::{GraphQLError, GraphQLErrorMessage};
use crate::ClientConfig;

#[derive(Clone, Debug)]
pub struct GQLClient {
endpoint: String,
timeout: u64,
header_map: HeaderMap,
config: ClientConfig,
}

#[derive(Serialize)]
Expand All @@ -30,45 +28,61 @@ struct GraphQLResponse<T> {

impl GQLClient {
#[cfg(target_arch = "wasm32")]
fn client(&self) -> Result<reqwest::Client, GraphQLError> {
fn client(&self) -> Result<Client, GraphQLError> {
Ok(Client::new())
}

#[cfg(not(target_arch = "wasm32"))]
fn client(&self) -> Result<reqwest::Client, GraphQLError> {
Client::builder()
.timeout(std::time::Duration::from_secs(self.timeout))
fn client(&self) -> Result<Client, GraphQLError> {
let mut builder = Client::builder().timeout(std::time::Duration::from_secs(
self.config.timeout.unwrap_or(5),
));
if let Some(proxy) = &self.config.proxy {
builder = builder.proxy(proxy.clone().try_into()?);
}
builder
.build()
.map_err(|e| GraphQLError::with_text(format!("Can not create client: {:?}", e)))
}
}

impl GQLClient {
pub fn new(endpoint: impl AsRef<str>, timeout: u64) -> Self {
pub fn new(endpoint: impl AsRef<str>) -> Self {
Self {
endpoint: endpoint.as_ref().to_string(),
timeout: timeout,
header_map: HeaderMap::new(),
config: ClientConfig {
endpoint: endpoint.as_ref().to_string(),
timeout: None,
headers: Default::default(),
proxy: None,
},
}
}

pub fn new_with_headers(endpoint: impl AsRef<str>, timeout: u64, headers: HashMap<&str, &str>) -> Self {
let mut header_map = HeaderMap::new();

for (str_key, str_value) in headers {
let key = HeaderName::from_str(str_key).unwrap();
let val = HeaderValue::from_str(str_value).unwrap();

header_map.insert(key, val);
}

pub fn new_with_headers(
endpoint: impl AsRef<str>,
headers: HashMap<impl ToString, impl ToString>,
) -> Self {
let _headers: HashMap<String, String> = headers
.iter()
.map(|(name, value)| (name.to_string(), value.to_string()))
.into_iter()
.collect();
Self {
endpoint: endpoint.as_ref().to_string(),
timeout: timeout,
header_map,
config: ClientConfig {
endpoint: endpoint.as_ref().to_string(),
timeout: None,
headers: Some(_headers),
proxy: None,
},
}
}

pub fn new_with_config(config: ClientConfig) -> Self {
Self { config }
}
}

impl GQLClient {
pub async fn query<K>(&self, query: &str) -> Result<Option<K>, GraphQLError>
where
K: for<'de> Deserialize<'de>,
Expand All @@ -95,7 +109,7 @@ impl GQLClient {
Some(v) => Ok(v),
None => Err(GraphQLError::with_text(format!(
"No data from graphql server({}) for this query",
self.endpoint
self.config.endpoint
))),
}
}
Expand All @@ -108,46 +122,107 @@ impl GQLClient {
where
K: for<'de> Deserialize<'de>,
{
let client: reqwest::Client = self.client()?;
self
.query_with_vars_by_endpoint(&self.config.endpoint, query, variables)
.await
}

async fn query_with_vars_by_endpoint<K, T: Serialize>(
&self,
endpoint: impl AsRef<str>,
query: &str,
variables: T,
) -> Result<Option<K>, GraphQLError>
where
K: for<'de> Deserialize<'de>,
{
let mut times = 1;
let mut endpoint = endpoint.as_ref().to_string();
let endpoint_url = Url::from_str(&endpoint)
.map_err(|e| GraphQLError::with_text(format!("Wrong endpoint: {}. {:?}", endpoint, e)))?;
let schema = endpoint_url.scheme();
let host = endpoint_url
.host()
.ok_or_else(|| GraphQLError::with_text(format!("Wrong endpoint: {}", endpoint)))?;

let client: Client = self.client()?;
let body = RequestBody {
query: query.to_string(),
variables,
};

let request = client
.post(&self.endpoint)
.json(&body)
.headers(self.header_map.clone());

let raw_response = request.send().await?;
let status = raw_response.status();
let response_body_text = raw_response
.text()
.await
.map_err(|e| GraphQLError::with_text(format!("Can not get response: {:?}", e)))?;

let json: GraphQLResponse<K> = serde_json::from_str(&response_body_text).map_err(|e| {
GraphQLError::with_text(format!(
"Failed to parse response: {:?}. The response body is: {}",
e, response_body_text
))
})?;

if !status.is_success() {
return Err(GraphQLError::with_message_and_json(
format!("The response is [{}]", status.as_u16()),
json.errors.unwrap_or_default(),
));
}

// Check if error messages have been received
if json.errors.is_some() {
return Err(GraphQLError::with_json(json.errors.unwrap_or_default()));
}
if json.data.is_none() {
log::warn!(target: "gql-client", "The deserialized data is none, the response is: {}", response_body_text);
loop {
if times > 10 {
return Err(GraphQLError::with_text(format!(
"Many redirect location: {}",
endpoint
)));
}

let mut request = client.post(&endpoint).json(&body);
if let Some(headers) = &self.config.headers {
if !headers.is_empty() {
for (name, value) in headers {
request = request.header(name, value);
}
}
}

let raw_response = request.send().await?;
if let Some(location) = raw_response.headers().get(reqwest::header::LOCATION) {
let redirect_url = location.to_str().map_err(|e| {
GraphQLError::with_text(format!(
"Failed to parse response header: Location. {:?}",
e
))
})?;

// if the response location start with http:// or https://
if redirect_url.starts_with("http://") || redirect_url.starts_with("https://") {
times += 1;
endpoint = redirect_url.to_string();
continue;
}

// without schema
endpoint = if redirect_url.starts_with('/') {
format!("{}://{}{}", schema, host, redirect_url)
} else {
format!("{}://{}/{}", schema, host, redirect_url)
};
times += 1;
continue;
}

let status = raw_response.status();
let response_body_text = raw_response
.text()
.await
.map_err(|e| GraphQLError::with_text(format!("Can not get response: {:?}", e)))?;

let json: GraphQLResponse<K> = serde_json::from_str(&response_body_text).map_err(|e| {
GraphQLError::with_text(format!(
"Failed to parse response: {:?}. The response body is: {}",
e, response_body_text
))
})?;

if !status.is_success() {
return Err(GraphQLError::with_message_and_json(
format!("The response is [{}]", status.as_u16()),
json.errors.unwrap_or_default(),
));
}

// Check if error messages have been received
if json.errors.is_some() {
return Err(GraphQLError::with_json(json.errors.unwrap_or_default()));
}
if json.data.is_none() {
log::warn!(target: "gql-client", "The deserialized data is none, the response is: {}", response_body_text);
}

return Ok(json.data);
}

Ok(json.data)
}
}
8 changes: 5 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
//! }
//! "#;
//!
//! let client = Client::new(endpoint, 5);
//! let client = Client::new(endpoint);
//! let vars = Vars { id: 1 };
//! let data = client.query_with_vars_unwrap::<Data, Vars>(query, vars).await.unwrap();
//!
Expand All @@ -68,7 +68,7 @@
//! let mut headers = HashMap::new();
//! headers.insert("authorization", "Bearer <some_token>");
//!
//! let client = Client::new_with_headers(endpoint, 5, headers);
//! let client = Client::new_with_headers(endpoint, headers);
//!
//! Ok(())
//!}
Expand Down Expand Up @@ -114,7 +114,7 @@
//! }
//! "#;
//!
//! let client = Client::new(endpoint, 5);
//! let client = Client::new(endpoint);
//! let vars = Vars { id: 1 };
//! let error = client.query_with_vars::<Data, Vars>(query, vars).await.err();
//!
Expand All @@ -126,7 +126,9 @@

mod client;
mod error;
mod types;

pub use client::GQLClient as Client;
pub use error::GraphQLError;
pub use error::GraphQLErrorMessage;
pub use types::*;
Loading

0 comments on commit 4b8189f

Please sign in to comment.