diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index f410d83..b7c07f7 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -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 @@ -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 @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2c07fc4..1c203cf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 8e6bd2d..a3d04cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,15 @@ [package] -name = "gql_client" -version = "1.0.6" -authors = ["Arthur Khlghatyan "] -edition = "2018" +name = "gql_client" +version = "1.0.7" +authors = ["Arthur Khlghatyan "] +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" } diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..22a9ce4 --- /dev/null +++ b/rust-toolchain.toml @@ -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"] diff --git a/src/client.rs b/src/client.rs index 52cf779..f985eef 100644 --- a/src/client.rs +++ b/src/client.rs @@ -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)] @@ -30,45 +28,61 @@ struct GraphQLResponse { impl GQLClient { #[cfg(target_arch = "wasm32")] - fn client(&self) -> Result { + fn client(&self) -> Result { Ok(Client::new()) } #[cfg(not(target_arch = "wasm32"))] - fn client(&self) -> Result { - Client::builder() - .timeout(std::time::Duration::from_secs(self.timeout)) + fn client(&self) -> Result { + 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, timeout: u64) -> Self { + pub fn new(endpoint: impl AsRef) -> 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, 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, + headers: HashMap, + ) -> Self { + let _headers: HashMap = 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(&self, query: &str) -> Result, GraphQLError> where K: for<'de> Deserialize<'de>, @@ -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 ))), } } @@ -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( + &self, + endpoint: impl AsRef, + query: &str, + variables: T, + ) -> Result, 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 = 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 = 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) } } diff --git a/src/lib.rs b/src/lib.rs index 18f4a59..cb5ac1f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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::(query, vars).await.unwrap(); //! @@ -68,7 +68,7 @@ //! let mut headers = HashMap::new(); //! headers.insert("authorization", "Bearer "); //! -//! let client = Client::new_with_headers(endpoint, 5, headers); +//! let client = Client::new_with_headers(endpoint, headers); //! //! Ok(()) //!} @@ -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::(query, vars).await.err(); //! @@ -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::*; diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..fc2550a --- /dev/null +++ b/src/types.rs @@ -0,0 +1,62 @@ +use std::collections::HashMap; +#[cfg(not(target_arch = "wasm32"))] +use std::convert::TryFrom; + +use serde::{Deserialize, Serialize}; + +#[cfg(not(target_arch = "wasm32"))] +use crate::GraphQLError; + +/// GQL client config +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ClientConfig { + /// the endpoint about graphql server + pub endpoint: String, + /// gql query timeout, unit: seconds + pub timeout: Option, + /// additional request header + pub headers: Option>, + /// request proxy + pub proxy: Option, +} + +/// proxy type +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum ProxyType { + Http, + Https, + All, +} + +/// proxy auth, basic_auth +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ProxyAuth { + pub username: String, + pub password: String, +} + +/// request proxy +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct GQLProxy { + /// schema, proxy url + pub schema: String, + /// proxy type + pub type_: ProxyType, + /// auth + pub auth: Option, +} + +#[cfg(not(target_arch = "wasm32"))] +impl TryFrom for reqwest::Proxy { + type Error = GraphQLError; + + fn try_from(gql_proxy: GQLProxy) -> Result { + let proxy = match gql_proxy.type_ { + ProxyType::Http => reqwest::Proxy::http(gql_proxy.schema), + ProxyType::Https => reqwest::Proxy::https(gql_proxy.schema), + ProxyType::All => reqwest::Proxy::all(gql_proxy.schema), + } + .map_err(|e| Self::Error::with_text(format!("{:?}", e)))?; + Ok(proxy) + } +} diff --git a/tests/errors.rs b/tests/errors.rs index e0e373f..c092e47 100644 --- a/tests/errors.rs +++ b/tests/errors.rs @@ -8,7 +8,7 @@ const ENDPOINT: &'static str = "https://graphqlzero.almansi.me/api"; #[tokio::test] pub async fn properly_parses_json_errors() { - let client = Client::new(ENDPOINT, 5); + let client = Client::new(ENDPOINT); // Send incorrect query let query = r#" diff --git a/tests/queries.rs b/tests/queries.rs index 3c88d57..d7f95a4 100644 --- a/tests/queries.rs +++ b/tests/queries.rs @@ -9,7 +9,7 @@ const ENDPOINT: &'static str = "https://graphqlzero.almansi.me/api"; #[tokio::test] pub async fn fetches_one_post() { - let client = Client::new(ENDPOINT, 5); + let client = Client::new(ENDPOINT); let query = r#" query SinglePostQuery($id: ID!) { @@ -33,7 +33,7 @@ pub async fn fetches_all_posts() { let mut headers = HashMap::new(); headers.insert("content-type", "application/json"); - let client = Client::new_with_headers(ENDPOINT, 5, headers); + let client = Client::new_with_headers(ENDPOINT, headers); let query = r#" query AllPostsQuery { diff --git a/tests/reqwest.rs b/tests/reqwest.rs new file mode 100644 index 0000000..5fa9aef --- /dev/null +++ b/tests/reqwest.rs @@ -0,0 +1,14 @@ +use reqwest::Url; +use std::str::FromStr; + +#[test] +fn test_url() { + let url_raw = "https://subql.darwinia.network/subql-bridger-darwinia"; + let url = Url::from_str(url_raw).unwrap(); + let schema = url.scheme(); + let host = url.host().unwrap(); + assert_eq!( + "https://subql.darwinia.network", + format!("{}://{}", schema, host.to_string()) + ); +}