From 2faabb0e502a9b15c88b0bdf5673d1b458198d70 Mon Sep 17 00:00:00 2001 From: Kasper Juul Hermansen Date: Sun, 19 Mar 2023 01:20:53 +0100 Subject: [PATCH] feat: with multi platform ci (#46) This adds the first iteration of multi platform ci. This is the lowest level of testing added: pinned all the way to nightly. Next up will be macos, then musl and arm. And lastly windows. Each will probably require special handling, especially because of how cross and qemu interacts with the dagger-engine and docker. --- .github/workflows/ci-multiplatform.yml | 121 ++++++++ Cargo.lock | 188 +++++++++---- ci/scripts/macos-install-packages | 4 + crates/dagger-bootstrap/Cargo.toml | 13 +- crates/dagger-core/Cargo.toml | 13 +- crates/dagger-core/src/gql_client.rs | 375 +++++++++++++++++++++++++ 6 files changed, 651 insertions(+), 63 deletions(-) create mode 100644 .github/workflows/ci-multiplatform.yml create mode 100755 ci/scripts/macos-install-packages create mode 100644 crates/dagger-core/src/gql_client.rs 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); + } + } +}