diff --git a/.github/workflows/ci-multiplatform.yml b/.github/workflows/ci-multiplatform.yml new file mode 100644 index 0000000..221ece0 --- /dev/null +++ b/.github/workflows/ci-multiplatform.yml @@ -0,0 +1,121 @@ +name: ci-multi-platform +on: + pull_request: + push: + branches: + - main + schedule: + - cron: '00 01 * * *' +jobs: + test: + name: test + env: + # For some builds, we use cross to test on 32-bit and big-endian + # systems. + CARGO: cargo + # When CARGO is set to CROSS, this is set to `--target matrix.target`. + TARGET_FLAGS: "" + # When CARGO is set to CROSS, TARGET_DIR includes matrix.target. + TARGET_DIR: ./target + # Emit backtraces on panics. + RUST_BACKTRACE: 1 + runs-on: ${{ matrix.os }} + strategy: + matrix: + build: + - pinned + - stable + - beta + - nightly + #- nightly-musl + #- nightly-32 + #- nightly-mips + #- nightly-arm + #- macos + # - win-msvc + #- win-gnu + include: + - build: pinned + os: ubuntu-22.04 + rust: 1.65.0 + - build: stable + os: ubuntu-22.04 + rust: stable + - build: beta + os: ubuntu-22.04 + rust: beta + - build: nightly + os: ubuntu-22.04 + rust: nightly + #- build: nightly-musl + # os: ubuntu-22.04 + # rust: nightly + # target: x86_64-unknown-linux-musl + #- build: nightly-32 + # os: ubuntu-22.04 + # rust: nightly + # target: i686-unknown-linux-gnu + #- build: nightly-mips + # os: ubuntu-22.04 + # rust: nightly + # target: mips64-unknown-linux-gnuabi64 + #- build: nightly-arm + # os: ubuntu-22.04 + # rust: nightly + # target: arm-unknown-linux-gnueabihf + #- build: macos + # os: macos-12 + # rust: nightly + #- build: win-msvc + # os: windows-2022 + # rust: nightly + #- build: win-gnu + # os: windows-2022 + # rust: nightly-x86_64-gnu + steps: + - name: Checkout repository + uses: actions/checkout@v3 + #- name: Install packages (Ubuntu) + # if: matrix.os == 'ubuntu-22.04' + # run: | + # ci/ubuntu-install-packages + - name: Install packages (macOS) + if: matrix.os == 'macos-12' + run: | + ci/scripts/macos-install-packages + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2.0.0 + - uses: actions-rs/toolchain@v1 + if: matrix.target == '' + with: + toolchain: ${{ matrix.rust }} + - uses: actions-rs/toolchain@v1 + if: matrix.target != '' + with: + toolchain: ${{ matrix.rust }} + target: ${{ matrix.target }} + use-cross: true + - uses: actions-rs/cargo@v1 + if: matrix.target != '' + with: + use-cross: true + command: build + args: --workspace --verbose --target ${{ matrix.target }} + - uses: actions-rs/cargo@v1 + if: matrix.target == '' + with: + command: build + args: --workspace --verbose + - uses: actions-rs/cargo@v1 + if: matrix.target != '' + with: + use-cross: true + command: test + args: --all --verbose --target ${{ matrix.target }} + - uses: actions-rs/cargo@v1 + if: matrix.target == '' + with: + command: test + args: --all --verbose diff --git a/Cargo.lock b/Cargo.lock index 95837c5..3432006 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,19 +23,6 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" -[[package]] -name = "async-compression" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a" -dependencies = [ - "flate2", - "futures-core", - "memchr", - "pin-project-lite", - "tokio", -] - [[package]] name = "async-trait" version = "0.1.67" @@ -132,9 +119,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.1.8" +version = "4.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5" +checksum = "ce38afc168d8665cfc75c7b1dd9672e50716a137f433f070991619744a67342a" dependencies = [ "bitflags", "clap_lex", @@ -145,9 +132,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" +checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646" dependencies = [ "os_str_bytes", ] @@ -265,8 +252,6 @@ dependencies = [ "dagger-core", "dirs", "eyre", - "flate2", - "graphql-introspection-query", "graphql_client", "hex", "hex-literal", @@ -304,7 +289,6 @@ dependencies = [ "eyre", "flate2", "gql_client", - "graphql-introspection-query", "graphql_client", "hex", "hex-literal", @@ -515,7 +499,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" dependencies = [ "crc32fast", - "libz-sys", "miniz_oxide", ] @@ -874,6 +857,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +dependencies = [ + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -930,10 +926,11 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3" +checksum = "0dd6da19f25979c7270e70fa95ab371ec3b701cd0eefc47667a09785b3c59155" dependencies = [ + "hermit-abi 0.3.1", "libc", "windows-sys 0.45.0", ] @@ -946,9 +943,9 @@ checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" [[package]] name = "is-terminal" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" +checksum = "8687c819457e979cc940d09cb16e42a1bf70aa6b60a549de6d3a62a0ee90c69e" dependencies = [ "hermit-abi 0.3.1", "io-lifetimes", @@ -992,17 +989,6 @@ version = "0.2.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" -[[package]] -name = "libz-sys" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - [[package]] name = "linux-raw-sys" version = "0.1.4" @@ -1125,9 +1111,9 @@ checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "openssl" -version = "0.10.45" +version = "0.10.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" +checksum = "fd2523381e46256e40930512c7fd25562b9eae4812cb52078f155e87217c9d1e" dependencies = [ "bitflags", "cfg-if", @@ -1157,9 +1143,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.80" +version = "0.9.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" +checksum = "176be2629957c157240f68f61f2d0053ad3a4ecfdd9ebf1e6521d18d9635cf67" dependencies = [ "autocfg", "cc", @@ -1374,7 +1360,6 @@ version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9" dependencies = [ - "async-compression", "base64", "bytes", "encoding_rs", @@ -1384,6 +1369,7 @@ dependencies = [ "http", "http-body", "hyper", + "hyper-rustls", "hyper-tls", "ipnet", "js-sys", @@ -1393,11 +1379,14 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-native-tls", + "tokio-rustls", "tokio-util", "tower-service", "url", @@ -1405,9 +1394,25 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", + "webpki-roots", "winreg", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -1416,9 +1421,9 @@ checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" [[package]] name = "rustix" -version = "0.36.9" +version = "0.36.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" +checksum = "2fe885c3a125aa45213b68cc1472a49880cb5923dc23f522ad2791b882228778" dependencies = [ "bitflags", "errno", @@ -1428,6 +1433,27 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "rustls" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +dependencies = [ + "base64", +] + [[package]] name = "ryu" version = "1.0.13" @@ -1449,6 +1475,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "security-framework" version = "2.8.2" @@ -1474,22 +1510,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.156" +version = "1.0.157" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "314b5b092c0ade17c00142951e50ced110ec27cea304b1037c6969246c2469a4" +checksum = "707de5fcf5df2b5788fca98dd7eab490bc2fd9b7ef1404defc462833b83f25ca" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.156" +version = "1.0.157" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7e29c4601e36bcec74a223228dce795f4cd3616341a4af93520ca1a837c087d" +checksum = "78997f4555c22a7971214540c4a661291970619afd56de19f77e0de86296e1e5" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.0", ] [[package]] @@ -1569,6 +1605,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "strsim" version = "0.10.0" @@ -1632,22 +1674,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.0", ] [[package]] @@ -1716,6 +1758,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + [[package]] name = "tokio-util" version = "0.7.7" @@ -1846,9 +1899,9 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicode-bidi" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524b68aca1d05e03fdf03fcdce2c6c94b6daf6d16861ddaa7e4f2b6638a9052c" +checksum = "7d502c968c6a838ead8e69b2ee18ec708802f99db92a0d156705ec9ef801993b" [[package]] name = "unicode-ident" @@ -1880,6 +1933,12 @@ dependencies = [ "void", ] +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.3.1" @@ -2020,6 +2079,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/ci/scripts/macos-install-packages b/ci/scripts/macos-install-packages new file mode 100755 index 0000000..358db2a --- /dev/null +++ b/ci/scripts/macos-install-packages @@ -0,0 +1,4 @@ +#!/bin/sh + +brew install docker +colima start diff --git a/crates/dagger-bootstrap/Cargo.toml b/crates/dagger-bootstrap/Cargo.toml index 49a0297..ad7f7a9 100644 --- a/crates/dagger-bootstrap/Cargo.toml +++ b/crates/dagger-bootstrap/Cargo.toml @@ -20,15 +20,20 @@ serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } +reqwest = { version = "0.11.14", features = [ + "stream", + "rustls", + "hyper-rustls", + "rustls-tls", +] } clap = "4.1.6" dirs = "4.0.0" -flate2 = { version = "1.0.25", features = ["zlib"] } -graphql-introspection-query = "0.2.0" -graphql_client = { version = "0.12.0", features = ["reqwest"] } +graphql_client = { version = "0.12.0", features = [ + "reqwest-rustls", +], default_features = false } hex = "0.4.3" hex-literal = "0.3.4" platform-info = "1.0.2" -reqwest = { version = "0.11.14", features = ["stream", "deflate"] } sha2 = "0.10.6" tar = "0.4.38" tempfile = "3.3.0" diff --git a/crates/dagger-core/Cargo.toml b/crates/dagger-core/Cargo.toml index cde1e4a..d82c691 100644 --- a/crates/dagger-core/Cargo.toml +++ b/crates/dagger-core/Cargo.toml @@ -20,13 +20,18 @@ tracing-subscriber = { workspace = true } base64 = "0.21.0" gql_client = "1.0.7" dirs = "4.0.0" -flate2 = { version = "1.0.25", features = ["zlib"] } -graphql-introspection-query = "0.2.0" -graphql_client = { version = "0.12.0", features = ["reqwest"] } +flate2 = { version = "1.0.25", features = ["rust_backend"] } +graphql_client = { version = "0.12.0", features = [ + "reqwest-rustls", + "graphql_query_derive", +], default-features = false } hex = "0.4.3" hex-literal = "0.3.4" platform-info = "1.0.2" -reqwest = { version = "0.11.14", features = ["stream", "deflate"] } +reqwest = { version = "0.11.14", features = [ + "stream", + "rustls-tls", +], default-features = false } sha2 = "0.10.6" tar = "0.4.38" tempfile = "3.3.0" diff --git a/crates/dagger-core/src/gql_client.rs b/crates/dagger-core/src/gql_client.rs new file mode 100644 index 0000000..a02263f --- /dev/null +++ b/crates/dagger-core/src/gql_client.rs @@ -0,0 +1,375 @@ +use reqwest::Error; +use reqwest::{Client, Url}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fmt::{self, Formatter}; +use std::str::FromStr; + +#[derive(Clone)] +pub struct GraphQLError { + message: String, + json: Option>, +} + +// https://spec.graphql.org/June2018/#sec-Errors +#[derive(Deserialize, Debug, Clone)] +#[allow(dead_code)] +pub struct GraphQLErrorMessage { + message: String, + locations: Option>, + extensions: Option>, + path: Option>, +} + +#[derive(Deserialize, Debug, Clone)] +#[allow(dead_code)] +pub struct GraphQLErrorLocation { + line: u32, + column: u32, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum GraphQLErrorPathParam { + String(String), + Number(u32), +} + +impl GraphQLError { + pub fn with_text(message: impl AsRef) -> Self { + Self { + message: message.as_ref().to_string(), + json: None, + } + } + + pub fn with_message_and_json(message: impl AsRef, json: Vec) -> Self { + Self { + message: message.as_ref().to_string(), + json: Some(json), + } + } + + pub fn with_json(json: Vec) -> Self { + Self::with_message_and_json("Look at json field for more details", json) + } + + pub fn message(&self) -> &str { + &self.message + } + + pub fn json(&self) -> Option> { + self.json.clone() + } +} + +fn format(err: &GraphQLError, f: &mut Formatter<'_>) -> fmt::Result { + // Print the main error message + writeln!(f, "\nGQLClient Error: {}", err.message)?; + + // Check if query errors have been received + if err.json.is_none() { + return Ok(()); + } + + let errors = err.json.as_ref(); + + for err in errors.unwrap() { + writeln!(f, "Message: {}", err.message)?; + } + + Ok(()) +} + +impl fmt::Display for GraphQLError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + format(self, f) + } +} + +impl fmt::Debug for GraphQLError { + #[allow(clippy::needless_borrow)] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + format(&self, f) + } +} + +impl From for GraphQLError { + fn from(error: Error) -> Self { + Self { + message: error.to_string(), + json: None, + } + } +} + +/// 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) + } +} + +#[derive(Clone, Debug)] +pub struct GQLClient { + config: ClientConfig, +} + +#[derive(Serialize)] +struct RequestBody { + query: String, + variables: T, +} + +#[derive(Deserialize, Debug)] +struct GraphQLResponse { + data: Option, + errors: Option>, +} + +impl GQLClient { + 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) -> Self { + Self { + config: ClientConfig { + endpoint: endpoint.as_ref().to_string(), + timeout: None, + headers: Default::default(), + proxy: None, + }, + } + } + + 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 { + 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>, + { + self.query_with_vars::(query, ()).await + } + + pub async fn query_unwrap(&self, query: &str) -> Result + where + K: for<'de> Deserialize<'de>, + { + self.query_with_vars_unwrap::(query, ()).await + } + + pub async fn query_with_vars_unwrap( + &self, + query: &str, + variables: T, + ) -> Result + where + K: for<'de> Deserialize<'de>, + { + match self.query_with_vars(query, variables).await? { + Some(v) => Ok(v), + None => Err(GraphQLError::with_text(format!( + "No data from graphql server({}) for this query", + self.config.endpoint + ))), + } + } + + pub async fn query_with_vars( + &self, + query: &str, + variables: T, + ) -> Result, GraphQLError> + where + K: for<'de> Deserialize<'de>, + { + 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, + }; + + 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() { + tracing::warn!( + target = "gql-client", + response_text = response_body_text, + "The deserialized data is none, the response", + ); + } + + return Ok(json.data); + } + } +}