diff --git a/.cargo/config b/.cargo/config deleted file mode 100644 index 4ec2f3b86..000000000 --- a/.cargo/config +++ /dev/null @@ -1,2 +0,0 @@ -[target.wasm32-unknown-unknown] -runner = 'wasm-bindgen-test-runner' diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..c353fc3f3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +Cargo.lock binary diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..9399d04b3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,42 @@ +version: 2 +updates: + # bump major and minor updates as soon as available + - package-ecosystem: cargo + target-branch: main # see https://github.com/dependabot/dependabot-core/issues/1778#issuecomment-1988140219 + directory: / + schedule: + interval: daily + commit-message: + prefix: chore + include: scope + ignore: + - dependency-name: "*" + update-types: + - "version-update:semver-patch" + + # bundle patch updates together on a monthly basis + - package-ecosystem: cargo + directory: / + schedule: + interval: monthly + commit-message: + prefix: chore + include: scope + groups: + patch-updates: + update-types: + - patch + ignore: + - dependency-name: "*" + update-types: + - "version-update:semver-minor" + - "version-update:semver-major" + + # bump actions as soon as available + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + commit-message: + prefix: chore + include: scope diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..d0fcb159f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,95 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: '0 2 * * *' + +env: + clippy_rust_version: '1.82' + +jobs: + test: + strategy: + matrix: + rust: ["stable", "beta", "nightly"] + os: [ubuntu-latest, macos-latest] + name: Cargo test + runs-on: ${{ matrix.os }} + if: github.repository == 'graphql-rust/graphql-client' + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Install toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + - name: Execute cargo test + run: cargo test --all --tests --examples + wasm_build: + name: Cargo build for wasm + runs-on: ubuntu-latest + if: github.repository == 'graphql-rust/graphql-client' + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - name: Install toolchain + uses: dtolnay/rust-toolchain@stable + with: + target: wasm32-unknown-unknown + - name: Execute cargo build + run: | + cargo build --manifest-path=./graphql_client/Cargo.toml --features="reqwest" --target wasm32-unknown-unknown + + rustfmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - run: cargo fmt --all -- --check + + lint: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.clippy_rust_version }} + components: clippy + - run: cargo clippy --all --all-targets --all-features -- -D warnings + + msrv: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Get MSRV from Cargo.toml + run: | + MSRV=$(grep 'rust-version' Cargo.toml | sed 's/.*= *"\(.*\)".*/\1/') + echo "MSRV=$MSRV" >> $GITHUB_ENV + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.MSRV }} + - uses: taiki-e/install-action@cargo-no-dev-deps + - run: cargo no-dev-deps check -p graphql_client + + # Automatically merge if it's a Dependabot PR that passes the build + dependabot: + needs: [test, wasm_build, lint, msrv] + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + if: github.actor == 'dependabot[bot]' + steps: + - name: Enable auto-merge for Dependabot PRs + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.gitignore b/.gitignore index 8af459eb9..8a761b9c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ -target/ +/target node_modules/ **/*.rs.bk -Cargo.lock .idea scripts/* !scripts/*.sh +/.vscode diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7819a61cd..000000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -language: rust -rust: - - stable - - beta - - nightly -cache: cargo -addons: - firefox: latest -before_install: - - if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (wget https://github.com/mozilla/geckodriver/releases/download/v0.23.0/geckodriver-v0.23.0-linux64.tar.gz) fi - - if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (mkdir geckodriver) fi - - if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (tar -xzf geckodriver-v0.23.0-linux64.tar.gz -C geckodriver) fi - - export PATH=$PATH:$PWD/geckodriver -before_script: - - if [ "$TRAVIS_RUST_VERSION" = "beta" ]; then (sudo apt-get update) fi - - if [ "$TRAVIS_RUST_VERSION" = "beta" ]; then (sudo apt-get install -y nodejs) fi - - if [ "$TRAVIS_RUST_VERSION" = "beta" ]; then (npm i -g prettier) fi - - if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (rustup component add rustfmt clippy) fi - - if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (rustup target add wasm32-unknown-unknown) fi - - if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (cargo install -f wasm-bindgen-cli) fi -script: - - if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (cargo fmt --all -- --check) fi - - if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (cargo clippy -- -D warnings) fi - - if [ "$TRAVIS_RUST_VERSION" = "beta" ]; then (prettier --debug-check -l './**/*.json' './**/*.graphql') fi - - cargo test --all - - cargo build --manifest-path=./examples/github/Cargo.toml - - cargo build --manifest-path=./examples/web/Cargo.toml - - if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then (xvfb-run cargo test --manifest-path=./graphql_client/Cargo.toml --features="web" --target wasm32-unknown-unknown) fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 3426ebc7d..c12aff5fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,69 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## Unreleased +## 0.14.0 - 2024-03-26 + +- Add support for GraphQL’s `extend type` directive +- Add support for `graphqls://` schema +- Expose `generate_module_token_stream_from_string` to allow custom macro wrappers + +## 0.13.0 - 2023-05-25 + +- Add support for `@oneOf` +- Update Ubuntu image for CI + +## 0.12.0 - 2023-01-12 + +- Switch to BTree to make codegen's output deterministic +- Add support for `skip_none` and `skip_serializing_none` +- Fix CI + +## 0.11.0 - 2022-06-21 + +- The `variables_derives` now trims whitespace from individual derivation traits. +- The new `reqwest-rustls` feature works like the `reqwest` feature but with + `rustls` rather than `native-tls`. +- Code generated by the `graphql-client` CLI program now suppresses all + warnings from rustc and clippy. +- Derive from both variables and response for enum +- Camel case input variables in .graphql files now generate snake_case variable names in Rust +- Support Paths in response derives +- cli: Display server responses when introspection failed +- upgrade to graphql_parser 0.4 +- Add support for `fragments-other-variant` + +## 0.10.0 - 2021-07-04 + +- The `web` feature is dropped. You can now use a `reqwest::Client` instead of the custom HTTP client. +- Allow specifying externally defined enums (thanks @jakmeier) +- Make the derive feature optional (but enabled by default) +- `--no-ssl` param in CLI (thanks @danielharbor!) +- The shape of some generated response types changed to be flatter and more ergonomic. +- Many dependencies were dropped +- A CLI and derive option to specify a module to import custom scalar from. + Thanks @miterst! ([PR](https://github.com/graphql-rust/graphql-client/pull/354)) + +## 0.9.0 - 2020-03-13 ## Added - The introspection query response shape internally used by graphql-client is now its own crate, `graphql-introspection-query`. +- A new experimental `normalization` attribute, that renames all + generated names to camel case. By default, the `"none"` naming conventions + are used, however, if set to `"rust"`, Rust naming rules will be used + instead. This may become the default in future versions, since not normalizing + names can lead to invalid code. (thanks @markcatley!) +- `response_derives` now only applies to responses. Use `variables_derives` for + variable structure derives. + +## Fixed + +- (BREAKING) In the CLI, there was a conflict between the short forms + `--output-directory` and `--selected-operation` since they were both `-o`. + After this fix, `-o` is the short form of `--output-directory`. (thanks @davidgraeff!) +- Catch more cases where a rust keyword in schemas or queries would break code generation (thanks @davidgraeff!) ## 0.8.0 - 2019-05-24 @@ -242,7 +298,10 @@ There are a number of breaking changes due to the new features, read the `Added` ### Added -- Copy documentation from the GraphQL schema to the generated types (including their fields) as normal Rust documentation. Documentation will show up in the generated docs as well as IDEs that support expanding derive macros (which does not include the RLS yet). +- Copy documentation from the GraphQL schema to the generated types (including + their fields) as normal Rust documentation. Documentation will show up in the + generated docs as well as IDEs that support expanding derive macros (which + does not include the RLS yet). - Implement and test deserializing subscription responses. We also try to provide helpful error messages when a subscription query is not valid (i.e. when it has more than one top-level field). - Support the [new top-level errors shape from the June 2018 spec](https://github.com/facebook/graphql/blob/master/spec/Section%207%20--%20Response.md), except for the `extensions` field (see issue #64). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d7a0841c2..b3028b872 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ All contributors are expected to follow our [Code of Conduct](CODE_OF_CONDUCT.md ## Pull requests -Before opening large pull requests, it is prefered that the change be discussed in a github issue first. This helps keep everyone on the same page, and facilitates a smoother code review process. +Before opening large pull requests, it is preferred that the change be discussed in a github issue first. This helps keep everyone on the same page, and facilitates a smoother code review process. ## Testing @@ -42,9 +42,7 @@ npm install --global prettier ### Running -Verify you are using the stable channel (output of `rustc --version` does not contain "nightly" or "beta"). Then run fmt, clippy, and test as they are invoked in the `.travis.yml` file. - -If you are on the stable channel, then you can run fmt, clippy, and test as they are invoked in the `.travis.yml` file. +If you are on the stable channel, then you can run fmt, clippy, and tests. ``` cargo fmt --all -- --check diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 000000000..5c3eb9881 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2194 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + +[[package]] +name = "cc" +version = "1.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clap" +version = "4.5.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "graphql-introspection-query" +version = "0.2.0" +dependencies = [ + "serde", +] + +[[package]] +name = "graphql-parser" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a818c0d883d7c0801df27be910917750932be279c7bc82dc541b8769425f409" +dependencies = [ + "combine", + "thiserror 1.0.69", +] + +[[package]] +name = "graphql_client" +version = "0.14.0" +dependencies = [ + "graphql_query_derive", + "reqwest", + "serde", + "serde_json", +] + +[[package]] +name = "graphql_client_cli" +version = "0.14.0" +dependencies = [ + "anstyle", + "clap", + "env_logger", + "graphql_client", + "graphql_client_codegen", + "log", + "reqwest", + "serde", + "serde_json", + "syn", +] + +[[package]] +name = "graphql_client_codegen" +version = "0.14.0" +dependencies = [ + "graphql-introspection-query", + "graphql-parser", + "heck", + "lazy_static", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", +] + +[[package]] +name = "graphql_query_derive" +version = "0.14.0" +dependencies = [ + "graphql_client_codegen", + "proc-macro2", + "syn", +] + +[[package]] +name = "graphql_query_github_example" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "env_logger", + "graphql_client", + "log", + "prettytable-rs", + "reqwest", +] + +[[package]] +name = "graphql_query_hasura_example" +version = "0.1.0" +dependencies = [ + "anyhow", + "env_logger", + "graphql_client", + "log", + "prettytable-rs", + "reqwest", + "serde_json", +] + +[[package]] +name = "h2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "is-terminal" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "log" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "openssl" +version = "0.10.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettytable-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a" +dependencies = [ + "csv", + "encode_unicode", + "is-terminal", + "lazy_static", + "term", + "unicode-width", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.11", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +dependencies = [ + "bytes", + "getrandom 0.2.15", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.11", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom 0.3.1", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.44.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web" +version = "0.1.0" +dependencies = [ + "graphql_client", + "js-sys", + "lazy_static", + "reqwest", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 82b1e89b7..a78811b81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,15 @@ [workspace] +resolver = "2" members = [ "graphql_client", "graphql_client_cli", "graphql_client_codegen", - "graphql_client_web", "graphql-introspection-query", "graphql_query_derive", + + # Example crates. + "examples/*", ] + +[workspace.package] +rust-version = "1.64.0" diff --git a/README.md b/README.md index bac197d32..7b86a93cf 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ # graphql_client -[![Build Status](https://travis-ci.org/graphql-rust/graphql-client.svg?branch=master)](https://travis-ci.org/graphql-rust/graphql-client) +[![Github actions Status](https://github.com/graphql-rust/graphql-client/workflows/CI/badge.svg?branch=main&event=push)](https://github.com/graphql-rust/graphql-client/actions) [![docs](https://docs.rs/graphql_client/badge.svg)](https://docs.rs/graphql_client/latest/graphql_client/) [![crates.io](https://img.shields.io/crates/v/graphql_client.svg)](https://crates.io/crates/graphql_client) -[![Join the chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/juniper-graphql/graphql-client) A typed GraphQL client library for Rust. @@ -19,7 +18,8 @@ A typed GraphQL client library for Rust. - Supports multiple operations per query document. - Supports setting GraphQL fields as deprecated and having the Rust compiler check their use. -- [web client](./graphql_client_web) for boilerplate-free API calls from browsers. +- Optional reqwest-based client for boilerplate-free API calls from browsers. +- Implicit and explicit null support. ## Getting started @@ -29,7 +29,7 @@ A typed GraphQL client library for Rust. - In order to provide precise types for a response, graphql_client needs to read the query and the schema at compile-time. - To download the schema, you have multiple options. This projects provides a [CLI](https://github.com/graphql-rust/graphql-client/tree/master/graphql_client_cli), however it does not matter what tool you use, the resulting `schema.json` is the same. + To download the schema, you have multiple options. This projects provides a [CLI](https://github.com/graphql-rust/graphql-client/tree/main/graphql_client_cli), however it does not matter what tool you use, the resulting `schema.json` is the same. - We now have everything we need to derive Rust types for our query. This is achieved through a procedural macro, as in the following snippet: @@ -58,6 +58,8 @@ A typed GraphQL client library for Rust. ```rust use graphql_client::{GraphQLQuery, Response}; + use std::error::Error; + use reqwest; #[derive(GraphQLQuery)] #[graphql( @@ -67,20 +69,29 @@ A typed GraphQL client library for Rust. )] pub struct UnionQuery; - fn perform_my_query(variables: union_query::Variables) -> Result<(), failure::Error> { + async fn perform_my_query(variables: union_query::Variables) -> Result<(), Box> { // this is the important line let request_body = UnionQuery::build_query(variables); let client = reqwest::Client::new(); - let mut res = client.post("/graphql").json(&request_body).send()?; - let response_body: Response = res.json()?; + let mut res = client.post("/graphql").json(&request_body).send().await?; + let response_body: Response = res.json().await?; println!("{:#?}", response_body); Ok(()) } ``` -[A complete example using the GitHub GraphQL API is available](https://github.com/graphql-rust/graphql-client/tree/master/examples/github), as well as sample [rustdoc output](https://www.tomhoule.com/docs/example_module/). +[A complete example using the GitHub GraphQL API is available](https://github.com/graphql-rust/graphql-client/tree/main/examples/github). + +## Alternative workflow using the CLI + +You can introspect GraphQL APIs and generate module from a command line interface to the library: + +```bash +$ cargo install graphql_client_cli +$ graphql-client --help +``` ## Deriving specific traits on the response @@ -97,10 +108,37 @@ use graphql_client::GraphQLQuery; )] struct UnionQuery; ``` +## Implicit Null + +The generated code will skip the serialization of `None` values. + +```rust +use graphql_client::GraphQLQuery; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "tests/unions/union_schema.graphql", + query_path = "tests/unions/union_query.graphql", + skip_serializing_none +)] +struct UnionQuery; +``` ## Custom scalars -The generated code will reference the scalar types as defined in the server schema. This means you have to provide matching rust types in the scope of the struct under derive. It can be as simple as declarations like `type Email = String;`. This gives you complete freedom on how to treat custom scalars, as long as they can be deserialized. +In GraphQL, five scalar types, `Int`, `Float`, `String`, `Boolean`, and `ID`, are available out of the box and are automatically mapped to equivalent types in Rust. However, in addition, custom scalar types can be defined by service providers by adding declarations like `scalar URI` to the server schema. + +If such custom scalar types are defined in the schema, depending on the content of the query, the generated code will also reference those scalar types. This means you have to provide matching Rust types in the scope of the struct under derive. It can be as simple as declarations like `type URI = String;`. This gives you complete freedom on how to treat custom scalars, as long as they can be deserialized. If such declarations are not provided, you will get build errors like this: + +``` +error[E0412]: cannot find type `URI` in module `super` + | + | #[derive(GraphQLQuery)] + | ^^^^^^^^^^^^ not found in `super` + | + = note: possible candidate is found in another module, you can import it into scope: + crate::repo_view::URI +``` ## Deprecations @@ -142,7 +180,7 @@ use graphql_client::GraphQLQuery; schema_path = "tests/unions/union_schema.graphql", query_path = "tests/unions/union_query.graphql", )] -pub struct UnionQuery; +pub struct Heights; ``` There is an example [in the tests](./graphql_client/tests/operation_selection). @@ -157,7 +195,7 @@ There is an [`include`](https://doc.rust-lang.org/cargo/reference/manifest.html# ## Examples -See the [examples directory](./graphql_client/examples) in this repository. +See the [examples directory](https://github.com/graphql-rust/graphql-client/tree/main/examples) in this repository. ## Contributors @@ -167,6 +205,7 @@ Warmest thanks to all those who contributed in any way (not only code) to this p - Ben Boeckel (@mathstuf) - Chris Fung (@aergonaut) - Christian Legnitto (@LegNeato) +- David Gräff (@davidgraeff) - Dirkjan Ochtman (@djc) - Fausto Nunez Alberro (@brainlessdeveloper) - Hirokazu Hata (@h-michael) @@ -178,7 +217,7 @@ Warmest thanks to all those who contributed in any way (not only code) to this p ## Code of conduct Anyone who interacts with this project in any space, including but not limited to -this GitHub repository, must follow our [code of conduct](https://github.com/graphql-rust/graphql-client/blob/master/CODE_OF_CONDUCT.md). +this GitHub repository, must follow our [code of conduct](https://github.com/graphql-rust/graphql-client/blob/main/CODE_OF_CONDUCT.md). ## License diff --git a/examples/github/Cargo.toml b/examples/github/Cargo.toml index 0423a0a2f..0a5d3636e 100644 --- a/examples/github/Cargo.toml +++ b/examples/github/Cargo.toml @@ -4,19 +4,11 @@ version = "0.1.0" authors = ["Tom Houlé "] edition = "2018" -[dependencies] -failure = "*" -graphql_client = { path = "../../graphql_client" } -serde = "^1.0" -serde_derive = "^1.0" -serde_json = "^1.0" -reqwest = "^0.9" -prettytable-rs = "^0.7" -structopt = "^0.2" -dotenv = "^0.13" -envy = "^0.3" +[dev-dependencies] +anyhow = "1.0" +graphql_client = { path = "../../graphql_client", features = ["reqwest-blocking"] } +reqwest = { version = "0.12", features = ["json", "blocking"] } +prettytable-rs = "^0.10.0" +clap = { version = "^4.0", features = ["derive"] } log = "^0.4" -env_logger = "^0.5" - -[workspace] -members = ["."] +env_logger = "0.10.2" diff --git a/examples/github/README.md b/examples/github/README.md index 005c7a603..35855b1dc 100644 --- a/examples/github/README.md +++ b/examples/github/README.md @@ -9,5 +9,5 @@ The example expects to find a valid GitHub API Token in the environment (`GITHUB Then just run the example with a repository name as argument. For example: ```bash -cargo run -- graphql-rust/graphql-client +cargo run --example github graphql-rust/graphql-client ``` diff --git a/examples/github/src/main.rs b/examples/github/examples/github.rs similarity index 51% rename from examples/github/src/main.rs rename to examples/github/examples/github.rs index f99823773..707d79f0c 100644 --- a/examples/github/src/main.rs +++ b/examples/github/examples/github.rs @@ -1,32 +1,29 @@ -use failure::*; -use graphql_client::*; +use ::reqwest::blocking::Client; +use anyhow::*; +use clap::Parser; +use graphql_client::{reqwest::post_graphql_blocking as post_graphql, GraphQLQuery}; use log::*; use prettytable::*; -use serde::*; -use structopt::StructOpt; +#[allow(clippy::upper_case_acronyms)] type URI = String; #[derive(GraphQLQuery)] #[graphql( - schema_path = "src/schema.graphql", - query_path = "src/query_1.graphql", + schema_path = "examples/schema.graphql", + query_path = "examples/query_1.graphql", response_derives = "Debug" )] struct RepoView; -#[derive(StructOpt)] +#[derive(Parser)] +#[clap(author, about, version)] struct Command { - #[structopt(name = "repository")] + #[clap(name = "repository")] repo: String, } -#[derive(Deserialize, Debug)] -struct Env { - github_api_token: String, -} - -fn parse_repo_name(repo_name: &str) -> Result<(&str, &str), failure::Error> { +fn parse_repo_name(repo_name: &str) -> Result<(&str, &str), anyhow::Error> { let mut parts = repo_name.split('/'); match (parts.next(), parts.next()) { (Some(owner), Some(name)) => Ok((owner, name)), @@ -34,41 +31,39 @@ fn parse_repo_name(repo_name: &str) -> Result<(&str, &str), failure::Error> { } } -fn main() -> Result<(), failure::Error> { - dotenv::dotenv().ok(); +fn main() -> Result<(), anyhow::Error> { env_logger::init(); - let config: Env = envy::from_env().context("while reading from environment")?; + let github_api_token = + std::env::var("GITHUB_API_TOKEN").expect("Missing GITHUB_API_TOKEN env var"); - let args = Command::from_args(); + let args = Command::parse(); let repo = args.repo; let (owner, name) = parse_repo_name(&repo).unwrap_or(("tomhoule", "graphql-client")); - let q = RepoView::build_query(repo_view::Variables { + let variables = repo_view::Variables { owner: owner.to_string(), name: name.to_string(), - }); - - let client = reqwest::Client::new(); - - let mut res = client - .post("https://api.github.com/graphql") - .bearer_auth(config.github_api_token) - .json(&q) - .send()?; + }; + + let client = Client::builder() + .user_agent("graphql-rust/0.10.0") + .default_headers( + std::iter::once(( + reqwest::header::AUTHORIZATION, + reqwest::header::HeaderValue::from_str(&format!("Bearer {}", github_api_token)) + .unwrap(), + )) + .collect(), + ) + .build()?; + + let response_body = + post_graphql::(&client, "https://api.github.com/graphql", variables).unwrap(); - let response_body: Response = res.json()?; info!("{:?}", response_body); - if let Some(errors) = response_body.errors { - println!("there are errors:"); - - for error in &errors { - println!("{:?}", error); - } - } - let response_data: repo_view::ResponseData = response_body.data.expect("missing response data"); let stars: Option = response_data @@ -79,19 +74,19 @@ fn main() -> Result<(), failure::Error> { println!("{}/{} - 🌟 {}", owner, name, stars.unwrap_or(0),); let mut table = prettytable::Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE); + table.set_titles(row!(b => "issue", "comments")); - table.add_row(row!(b => "issue", "comments")); - - for issue in &response_data + for issue in response_data .repository .expect("missing repository") .issues .nodes .expect("issue nodes is null") + .iter() + .flatten() { - if let Some(issue) = issue { - table.add_row(row!(issue.title, issue.comments.total_count)); - } + table.add_row(row!(issue.title, issue.comments.total_count)); } table.printstd(); diff --git a/examples/github/src/query_1.graphql b/examples/github/examples/query_1.graphql similarity index 100% rename from examples/github/src/query_1.graphql rename to examples/github/examples/query_1.graphql diff --git a/examples/github/src/schema.graphql b/examples/github/examples/schema.graphql similarity index 99% rename from examples/github/src/schema.graphql rename to examples/github/examples/schema.graphql index c13a47a40..92ee5b0a5 100644 --- a/examples/github/src/schema.graphql +++ b/examples/github/examples/schema.graphql @@ -2410,7 +2410,7 @@ type IssueTimelineConnection { "An item in an issue timeline" union IssueTimelineItem = - AssignedEvent + | AssignedEvent | ClosedEvent | Commit | CrossReferencedEvent @@ -5009,7 +5009,7 @@ type PullRequestTimelineConnection { "An item in an pull request timeline" union PullRequestTimelineItem = - AssignedEvent + | AssignedEvent | BaseRefForcePushedEvent | ClosedEvent | Commit @@ -6940,7 +6940,7 @@ type ReviewRequestedEvent implements Node { "The results of a search." union SearchResultItem = - Issue + | Issue | MarketplaceListing | Organization | PullRequest diff --git a/examples/hasura/Cargo.toml b/examples/hasura/Cargo.toml new file mode 100644 index 000000000..46d66a333 --- /dev/null +++ b/examples/hasura/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "graphql_query_hasura_example" +version = "0.1.0" +authors = ["Mark Catley "] +edition = "2018" + +[dev-dependencies] +anyhow = "1.0" +graphql_client = { path = "../../graphql_client", features = ["reqwest-blocking"] } +serde_json = "1.0" +reqwest = { version = "0.12", features = ["json", "blocking"] } +prettytable-rs = "0.10.0" +log = "0.4.3" +env_logger = "0.10.2" diff --git a/examples/hasura/README.md b/examples/hasura/README.md new file mode 100644 index 000000000..0cba48ac6 --- /dev/null +++ b/examples/hasura/README.md @@ -0,0 +1,6 @@ +# graphql-client Hasura examples + +The schema was generated using [Hasura](https://hasura.io/). It is here to +demonstrate the `normalization` attribute and would require some work to +create a Hasura instance that matches the schema. It is primarily present to +ensure the attribute behaves correctly. diff --git a/examples/hasura/examples/hasura.rs b/examples/hasura/examples/hasura.rs new file mode 100644 index 000000000..0fe9c4672 --- /dev/null +++ b/examples/hasura/examples/hasura.rs @@ -0,0 +1,62 @@ +use ::reqwest::blocking::Client; +use graphql_client::{reqwest::post_graphql_blocking as post_graphql, GraphQLQuery}; +use log::*; +use prettytable::*; + +type Bpchar = String; +type Timestamptz = String; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "examples/schema.graphql", + query_path = "examples/query_1.graphql", + response_derives = "Debug", + normalization = "rust" +)] +struct UpsertIssue; + +fn main() -> Result<(), anyhow::Error> { + use upsert_issue::{IssuesUpdateColumn::*, *}; + env_logger::init(); + + let v = Variables { + issues: vec![IssuesInsertInput { + id: Some("001000000000000".to_string()), + name: Some("Name".to_string()), + status: Some("Draft".to_string()), + salesforce_updated_at: Some("2019-06-11T08:14:28Z".to_string()), + }], + update_columns: vec![Name, Status, SalesforceUpdatedAt], + }; + + let client = Client::new(); + + let response_body = + post_graphql::(&client, "https://localhost:8080/v1/graphql", v)?; + info!("{:?}", response_body); + + if let Some(errors) = response_body.errors { + error!("there are errors:"); + + for error in &errors { + error!("{:?}", error); + } + } + + let response_data = response_body.data.expect("missing response data"); + + let mut table = prettytable::Table::new(); + + table.add_row(row!(b => "id", "name")); + + for issue in &response_data + .insert_issues + .expect("Inserted Issues") + .returning + { + table.add_row(row!(issue.id, issue.name)); + } + + table.printstd(); + Ok(()) +} diff --git a/examples/hasura/examples/query_1.graphql b/examples/hasura/examples/query_1.graphql new file mode 100644 index 000000000..d8fb2daa1 --- /dev/null +++ b/examples/hasura/examples/query_1.graphql @@ -0,0 +1,14 @@ +mutation upsert_issue( + $issues: [issues_insert_input!]! + $update_columns: [issues_update_column!]! +) { + insert_issues( + objects: $issues + on_conflict: { constraint: issues_pkey, update_columns: $update_columns } + ) { + returning { + id + name + } + } +} diff --git a/examples/hasura/examples/schema.graphql b/examples/hasura/examples/schema.graphql new file mode 100644 index 000000000..3309b4b43 --- /dev/null +++ b/examples/hasura/examples/schema.graphql @@ -0,0 +1,349 @@ +schema { + query: query_root + mutation: mutation_root + subscription: subscription_root +} + +scalar bpchar + +# expression to compare columns of type bpchar. All fields are combined with logical 'AND'. +input bpchar_comparison_exp { + _eq: bpchar + _gt: bpchar + _gte: bpchar + _in: [bpchar!] + _is_null: Boolean + _lt: bpchar + _lte: bpchar + _neq: bpchar + _nin: [bpchar!] +} + +# conflict action +enum conflict_action { + # ignore the insert on this row + ignore + + # update the row with the given values + update +} + +# columns and relationships of "issues" +type issues { + id: bpchar! + name: String! + salesforce_updated_at: timestamptz! + status: String! +} + +# aggregated selection of "issues" +type issues_aggregate { + aggregate: issues_aggregate_fields + nodes: [issues!]! +} + +# aggregate fields of "issues" +type issues_aggregate_fields { + count(columns: [issues_select_column!], distinct: Boolean): Int + max: issues_max_fields + min: issues_min_fields +} + +# order by aggregate values of table "issues" +input issues_aggregate_order_by { + count: order_by + max: issues_max_order_by + min: issues_min_order_by +} + +# input type for inserting array relation for remote table "issues" +input issues_arr_rel_insert_input { + data: [issues_insert_input!]! + on_conflict: issues_on_conflict +} + +# Boolean expression to filter rows from the table "issues". All fields are combined with a logical 'AND'. +input issues_bool_exp { + _and: [issues_bool_exp] + _not: issues_bool_exp + _or: [issues_bool_exp] + id: bpchar_comparison_exp + name: text_comparison_exp + salesforce_updated_at: timestamptz_comparison_exp + status: text_comparison_exp +} + +# unique or primary key constraints on table "issues" +enum issues_constraint { + # unique or primary key constraint + issues_pkey +} + +# input type for inserting data into table "issues" +input issues_insert_input { + id: bpchar + name: String + salesforce_updated_at: timestamptz + status: String +} + +# aggregate max on columns +type issues_max_fields { + name: String + salesforce_updated_at: timestamptz + status: String +} + +# order by max() on columns of table "issues" +input issues_max_order_by { + name: order_by + salesforce_updated_at: order_by + status: order_by +} + +# aggregate min on columns +type issues_min_fields { + name: String + salesforce_updated_at: timestamptz + status: String +} + +# order by min() on columns of table "issues" +input issues_min_order_by { + name: order_by + salesforce_updated_at: order_by + status: order_by +} + +# response of any mutation on the table "issues" +type issues_mutation_response { + # number of affected rows by the mutation + affected_rows: Int! + + # data of the affected rows by the mutation + returning: [issues!]! +} + +# input type for inserting object relation for remote table "issues" +input issues_obj_rel_insert_input { + data: issues_insert_input! + on_conflict: issues_on_conflict +} + +# on conflict condition type for table "issues" +input issues_on_conflict { + constraint: issues_constraint! + update_columns: [issues_update_column!]! +} + +# ordering options when selecting data from "issues" +input issues_order_by { + id: order_by + name: order_by + salesforce_updated_at: order_by + status: order_by +} + +# select columns of table "issues" +enum issues_select_column { + # column name + id + + # column name + name + + # column name + salesforce_updated_at + + # column name + status +} + +# input type for updating data in table "issues" +input issues_set_input { + id: bpchar + name: String + salesforce_updated_at: timestamptz + status: String +} + +# update columns of table "issues" +enum issues_update_column { + # column name + id + + # column name + name + + # column name + salesforce_updated_at + + # column name + status +} + +# mutation root +type mutation_root { + # delete data from the table: "issues" + delete_issues( + # filter the rows which have to be deleted + where: issues_bool_exp! + ): issues_mutation_response + + # insert data into the table: "issues" + insert_issues( + # the rows to be inserted + objects: [issues_insert_input!]! + + # on conflict condition + on_conflict: issues_on_conflict + ): issues_mutation_response + + # update data of the table: "issues" + update_issues( + # sets the columns of the filtered rows to the given values + _set: issues_set_input + + # filter the rows which have to be updated + where: issues_bool_exp! + ): issues_mutation_response +} + +# column ordering options +enum order_by { + # in the ascending order, nulls last + asc + + # in the ascending order, nulls first + asc_nulls_first + + # in the ascending order, nulls last + asc_nulls_last + + # in the descending order, nulls first + desc + + # in the descending order, nulls first + desc_nulls_first + + # in the descending order, nulls last + desc_nulls_last +} + +# query root +type query_root { + # fetch data from the table: "issues" + issues( + # distinct select on columns + distinct_on: [issues_select_column!] + + # limit the nuber of rows returned + limit: Int + + # skip the first n rows. Use only with order_by + offset: Int + + # sort the rows by one or more columns + order_by: [issues_order_by!] + + # filter the rows returned + where: issues_bool_exp + ): [issues!]! + + # fetch aggregated fields from the table: "issues" + issues_aggregate( + # distinct select on columns + distinct_on: [issues_select_column!] + + # limit the nuber of rows returned + limit: Int + + # skip the first n rows. Use only with order_by + offset: Int + + # sort the rows by one or more columns + order_by: [issues_order_by!] + + # filter the rows returned + where: issues_bool_exp + ): issues_aggregate! + + # fetch data from the table: "issues" using primary key columns + issues_by_pk(id: bpchar!): issues +} + +# subscription root +type subscription_root { + # fetch data from the table: "issues" + issues( + # distinct select on columns + distinct_on: [issues_select_column!] + + # limit the nuber of rows returned + limit: Int + + # skip the first n rows. Use only with order_by + offset: Int + + # sort the rows by one or more columns + order_by: [issues_order_by!] + + # filter the rows returned + where: issues_bool_exp + ): [issues!]! + + # fetch aggregated fields from the table: "issues" + issues_aggregate( + # distinct select on columns + distinct_on: [issues_select_column!] + + # limit the nuber of rows returned + limit: Int + + # skip the first n rows. Use only with order_by + offset: Int + + # sort the rows by one or more columns + order_by: [issues_order_by!] + + # filter the rows returned + where: issues_bool_exp + ): issues_aggregate! + + # fetch data from the table: "issues" using primary key columns + issues_by_pk(id: bpchar!): issues +} + +# expression to compare columns of type text. All fields are combined with logical 'AND'. +input text_comparison_exp { + _eq: String + _gt: String + _gte: String + _ilike: String + _in: [String!] + _is_null: Boolean + _like: String + _lt: String + _lte: String + _neq: String + _nilike: String + _nin: [String!] + _nlike: String + _nsimilar: String + _similar: String +} + +scalar timestamptz + +# expression to compare columns of type timestamptz. All fields are combined with logical 'AND'. +input timestamptz_comparison_exp { + _eq: timestamptz + _gt: timestamptz + _gte: timestamptz + _in: [timestamptz!] + _is_null: Boolean + _lt: timestamptz + _lte: timestamptz + _neq: timestamptz + _nin: [timestamptz!] +} diff --git a/examples/web/Cargo.toml b/examples/web/Cargo.toml index a02629ff5..ae65e942f 100644 --- a/examples/web/Cargo.toml +++ b/examples/web/Cargo.toml @@ -4,22 +4,16 @@ version = "0.1.0" authors = ["Tom Houlé "] edition = "2018" -[profile.release] -lto = "thin" - [lib] -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] [dependencies] -graphql_client = { path = "../../graphql_client" } -graphql_client_web = { path = "../../graphql_client_web" } -wasm-bindgen = "0.2.43" -serde = { version = "1.0.67", features = ["derive"] } -serde_json = "1.0.22" +graphql_client = { path = "../../graphql_client", features = ["reqwest"] } +wasm-bindgen = "^0.2" lazy_static = "1.0.1" js-sys = "0.3.6" -futures = "0.1.25" -wasm-bindgen-futures = "0.3.6" +wasm-bindgen-futures = "0.4.18" +reqwest = "0.12" [dependencies.web-sys] version = "0.3.6" @@ -32,7 +26,5 @@ features = [ "HtmlBodyElement", "HtmlDocument", "HtmlElement", + "Window", ] - -[workspace] -members = ["."] diff --git a/examples/web/src/lib.rs b/examples/web/src/lib.rs index 2bbe17527..cfb230202 100644 --- a/examples/web/src/lib.rs +++ b/examples/web/src/lib.rs @@ -1,5 +1,4 @@ -use futures::Future; -use graphql_client::GraphQLQuery; +use graphql_client::{reqwest::post_graphql, GraphQLQuery}; use lazy_static::*; use std::cell::RefCell; use std::sync::Mutex; @@ -23,28 +22,25 @@ lazy_static! { static ref LAST_ENTRY: Mutex>> = Mutex::new(RefCell::new(None)); } -fn load_more() -> impl Future { - let client = graphql_client::web::Client::new("https://www.graphqlhub.com/graphql"); +async fn load_more() -> Result { + let url = "https://www.graphqlhub.com/graphql"; let variables = puppy_smiles::Variables { after: LAST_ENTRY .lock() .ok() .and_then(|opt| opt.borrow().to_owned()), }; - let response = client.call(PuppySmiles, variables); - response - .map(|response| { - render_response(response); - JsValue::NULL - }) + let client = reqwest::Client::new(); + + let response = post_graphql::(&client, url, variables) + .await .map_err(|err| { - log(&format!( - "Could not fetch puppies. graphql_client_web error: {:?}", - err - )); + log(&format!("Could not fetch puppies. error: {:?}", err)); JsValue::NULL - }) + })?; + render_response(response); + Ok(JsValue::NULL) } fn document() -> web_sys::Document { @@ -64,7 +60,7 @@ fn add_load_more_button() { ); btn.add_event_listener_with_callback( "click", - &on_click + on_click .as_ref() .dyn_ref() .expect_throw("on click is not a Function"), @@ -78,14 +74,14 @@ fn add_load_more_button() { on_click.forget(); } -fn render_response(response: graphql_client_web::Response) { +fn render_response(response: graphql_client::Response) { use std::fmt::Write; log(&format!("response body\n\n{:?}", response)); let parent = document().body().expect_throw("no body"); - let json: graphql_client_web::Response = response; + let json: graphql_client::Response = response; let response = document() .create_element("div") .expect_throw("could not create div"); @@ -101,15 +97,13 @@ fn render_response(response: graphql_client_web::Response = listings[listings.len() - 1] .as_ref() - .map(|puppy| puppy.fullname_id.clone()) - .to_owned(); + .map(|puppy| puppy.fullname_id.clone()); LAST_ENTRY.lock().unwrap_throw().replace(new_cursor); - for puppy in &listings { - if let Some(puppy) = puppy { - write!( - inner_html, - r#" + for puppy in listings.iter().flatten() { + write!( + inner_html, + r#"
{}
@@ -117,10 +111,9 @@ fn render_response(response: graphql_client_web::Response
"#, - puppy.title, puppy.url, puppy.title - ) - .expect_throw("write to string"); - } + puppy.title, puppy.url, puppy.title + ) + .expect_throw("write to string"); } response.set_inner_html(&format!( "

response:

{}
", @@ -143,7 +136,6 @@ pub fn run() { .append_child(&message_area) .expect_throw("could not append message area"); - load_more(); add_load_more_button(); log("Bye"); diff --git a/graphql-introspection-query/Cargo.toml b/graphql-introspection-query/Cargo.toml index bdcb13f79..b7aa1b6e0 100644 --- a/graphql-introspection-query/Cargo.toml +++ b/graphql-introspection-query/Cargo.toml @@ -1,10 +1,13 @@ [package] name = "graphql-introspection-query" -version = "0.1.0" +version = "0.2.0" authors = ["Tom Houlé "] edition = "2018" keywords = ["graphql", "api", "web"] categories = ["web-programming"] +license = "Apache-2.0 OR MIT" +repository = "https://github.com/graphql-rust/graphql-client" +description = "GraphQL introspection query and response types." # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/graphql-introspection-query/src/introspection_response.rs b/graphql-introspection-query/src/introspection_response.rs index 9e7d1fe8e..63d673c17 100644 --- a/graphql-introspection-query/src/introspection_response.rs +++ b/graphql-introspection-query/src/introspection_response.rs @@ -78,7 +78,7 @@ impl<'de> Deserialize<'de> for __DirectiveLocation { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum __TypeKind { SCALAR, OBJECT, @@ -130,17 +130,18 @@ pub struct FullType { pub kind: Option<__TypeKind>, pub name: Option, pub description: Option, - pub fields: Option>>, - pub input_fields: Option>>, - pub interfaces: Option>>, - pub enum_values: Option>>, - pub possible_types: Option>>, + pub fields: Option>, + pub input_fields: Option>, + pub interfaces: Option>, + pub enum_values: Option>, + pub possible_types: Option>, } #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FullTypeFieldsArgs { #[serde(flatten)] + #[allow(dead_code)] input_value: InputValue, } @@ -196,19 +197,14 @@ pub struct FullTypePossibleTypes { #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct InputValue { - pub name: Option, + pub name: String, pub description: Option, #[serde(rename = "type")] - pub type_: Option, + pub type_: InputValueType, pub default_value: Option, } -#[derive(Clone, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct InputValueType { - #[serde(flatten)] - pub type_ref: TypeRef, -} +type InputValueType = TypeRef; #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -247,6 +243,7 @@ pub struct SchemaTypes { #[serde(rename_all = "camelCase")] pub struct SchemaDirectivesArgs { #[serde(flatten)] + #[allow(dead_code)] input_value: InputValue, } @@ -266,6 +263,7 @@ pub struct Schema { pub mutation_type: Option, pub subscription_type: Option, pub types: Option>>, + #[allow(dead_code)] directives: Option>>, } @@ -291,7 +289,7 @@ impl IntrospectionResponse { pub fn as_schema(&self) -> &SchemaContainer { match self { IntrospectionResponse::FullResponse(full_response) => &full_response.data, - IntrospectionResponse::Schema(schema) => &schema, + IntrospectionResponse::Schema(schema) => schema, } } diff --git a/graphql_client/Cargo.toml b/graphql_client/Cargo.toml index b35803eb3..680d70689 100644 --- a/graphql_client/Cargo.toml +++ b/graphql_client/Cargo.toml @@ -1,65 +1,30 @@ [package] name = "graphql_client" -version = "0.8.0" +version = "0.14.0" authors = ["Tom Houlé "] description = "Typed GraphQL requests and responses" repository = "https://github.com/graphql-rust/graphql-client" license = "Apache-2.0 OR MIT" -keywords = ["graphql", "api", "web", "webassembly", "wasm"] +keywords = ["graphql", "api", "web", "webassembly", "wasm"] categories = ["network-programming", "web-programming", "wasm"] edition = "2018" +homepage = "https://github.com/graphql-rust/graphql-client" +readme = "../README.md" +rust-version.workspace = true -[dependencies] -doc-comment = "^0.3" -failure = { version = "0.1", optional = true } -graphql_query_derive = { path = "../graphql_query_derive", version = "0.8.0" } -serde_json = "1.0" -serde = { version = "^1.0.78", features = ["derive"] } - -[dependencies.futures] -version = "^0.1" -optional = true - -[dependencies.js-sys] -version = "^0.3" -optional = true - -[dependencies.log] -version = "^0.4" -optional = true +[package.metadata.docs.rs] +features = ["reqwest"] -[dependencies.web-sys] -version = "^0.3" -optional = true -features = [ - "Headers", - "Request", - "RequestInit", - "Response", - "Window", -] - -[dependencies.wasm-bindgen] -version = "^0.2" -optional = true - -[dependencies.wasm-bindgen-futures] -version = "^0.3" -optional = true - -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -reqwest = "^0.9" +[dependencies] +serde = { version = "1.0.78", features = ["derive"] } +serde_json = "1.0.50" -[dev-dependencies] -wasm-bindgen-test = "0.2.43" +# Optional dependencies +graphql_query_derive = { path = "../graphql_query_derive", version = "0.14.0", optional = true } +reqwest-crate = { package = "reqwest", version = ">=0.11, <=0.12", features = ["json"], default-features = false, optional = true } [features] -web = [ - "failure", - "futures", - "js-sys", - "log", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] +default = ["graphql_query_derive"] +reqwest = ["reqwest-crate", "reqwest-crate/default-tls"] +reqwest-rustls = ["reqwest-crate", "reqwest-crate/rustls-tls"] +reqwest-blocking = ["reqwest-crate/blocking"] diff --git a/graphql_client/src/lib.rs b/graphql_client/src/lib.rs index 2847f3052..67364730e 100644 --- a/graphql_client/src/lib.rs +++ b/graphql_client/src/lib.rs @@ -1,27 +1,40 @@ -//! The top-level documentation resides on the [project README](https://github.com/graphql-rust/graphql-client) at the moment. +//! The top-level documentation resides on the [project +//! README](https://github.com/graphql-rust/graphql-client) at the moment. //! -//! The main interface to this library is the custom derive that generates modules from a GraphQL query and schema. See the docs for the [`GraphQLQuery`] trait for a full example. +//! The main interface to this library is the custom derive that generates +//! modules from a GraphQL query and schema. See the docs for the +//! [`GraphQLQuery`] trait for a full example. +//! +//! ## Cargo features +//! +//! - `graphql_query_derive` (default: on): enables the `#[derive(GraphqlQuery)]` custom derive. +//! - `reqwest` (default: off): exposes the `graphql_client::reqwest::post_graphql()` function. +//! - `reqwest-blocking` (default: off): exposes the blocking version, `graphql_client::reqwest::post_graphql_blocking()`. #![deny(missing_docs)] -#![deny(rust_2018_idioms)] -#![deny(warnings)] +#![warn(rust_2018_idioms)] +#[cfg(feature = "graphql_query_derive")] #[allow(unused_imports)] #[macro_use] extern crate graphql_query_derive; +#[cfg(feature = "graphql_query_derive")] #[doc(hidden)] pub use graphql_query_derive::*; -use serde::*; +#[cfg(any( + feature = "reqwest", + feature = "reqwest-rustls", + feature = "reqwest-blocking" +))] +pub mod reqwest; -#[cfg(feature = "web")] -pub mod web; +pub mod serde_with; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::fmt::{self, Display}; - -doc_comment::doctest!("../../README.md"); +use std::fmt::{self, Display, Write}; /// A convenience trait that can be used to build a GraphQL request body. /// @@ -32,6 +45,7 @@ doc_comment::doctest!("../../README.md"); /// ``` /// use graphql_client::*; /// use serde_json::json; +/// use std::error::Error; /// /// #[derive(GraphQLQuery)] /// #[graphql( @@ -40,7 +54,7 @@ doc_comment::doctest!("../../README.md"); /// )] /// struct StarWarsQuery; /// -/// fn main() -> Result<(), failure::Error> { +/// fn main() -> Result<(), Box> { /// use graphql_client::GraphQLQuery; /// /// let variables = star_wars_query::Variables { @@ -76,10 +90,7 @@ pub trait GraphQLQuery { /// The form in which queries are sent over HTTP in most implementations. This will be built using the [`GraphQLQuery`] trait normally. #[derive(Debug, Serialize, Deserialize)] -pub struct QueryBody -where - Variables: serde::Serialize, -{ +pub struct QueryBody { /// The values for the variables. They must match those declared in the queries. This should be the `Variables` struct from the generated module corresponding to the query. pub variables: Variables, /// The GraphQL query, as a string. @@ -90,7 +101,7 @@ where } /// Represents a location inside a query string. Used in errors. See [`Error`]. -#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct Location { /// The line number in the query string where the error originated (starting from 1). pub line: i32, @@ -99,7 +110,7 @@ pub struct Location { } /// Part of a path in a query. It can be an object key or an array index. See [`Error`]. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(untagged)] pub enum PathFragment { /// A key inside an object @@ -128,13 +139,14 @@ impl Display for PathFragment { /// # use serde_json::json; /// # use serde::Deserialize; /// # use graphql_client::GraphQLQuery; +/// # use std::error::Error; /// # /// # #[derive(Debug, Deserialize, PartialEq)] /// # struct ResponseData { /// # something: i32 /// # } /// # -/// # fn main() -> Result<(), failure::Error> { +/// # fn main() -> Result<(), Box> { /// use graphql_client::*; /// /// let body: Response = serde_json::from_value(json!({ @@ -175,6 +187,7 @@ impl Display for PathFragment { /// extensions: None, /// }, /// ]), +/// extensions: None, /// }; /// /// assert_eq!(body, expected); @@ -182,7 +195,7 @@ impl Display for PathFragment { /// # Ok(()) /// # } /// ``` -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Error { /// The human-readable error message. This is the only required field. pub message: String, @@ -204,7 +217,7 @@ impl Display for Error { fragments .iter() .fold(String::new(), |mut acc, item| { - acc.push_str(&format!("{}/", item)); + let _ = write!(acc, "{}/", item); acc }) .trim_end_matches('/') @@ -218,7 +231,7 @@ impl Display for Error { .as_ref() .and_then(|locations| locations.iter().next()) .cloned() - .unwrap_or_else(Location::default); + .unwrap_or_default(); write!(f, "{}:{}:{}: {}", path, loc.line, loc.column, self.message) } @@ -234,6 +247,7 @@ impl Display for Error { /// # use serde_json::json; /// # use serde::Deserialize; /// # use graphql_client::GraphQLQuery; +/// # use std::error::Error; /// # /// # #[derive(Debug, Deserialize, PartialEq)] /// # struct User { @@ -251,7 +265,7 @@ impl Display for Error { /// # dogs: Vec, /// # } /// # -/// # fn main() -> Result<(), failure::Error> { +/// # fn main() -> Result<(), Box> { /// use graphql_client::Response; /// /// let body: Response = serde_json::from_value(json!({ @@ -268,6 +282,7 @@ impl Display for Error { /// dogs: vec![Dog { name: "Strelka".to_owned() }], /// }), /// errors: Some(vec![]), +/// extensions: None, /// }; /// /// assert_eq!(body, expected); @@ -275,12 +290,21 @@ impl Display for Error { /// # Ok(()) /// # } /// ``` -#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Response { /// The absent, partial or complete response data. pub data: Option, /// The top-level errors returned by the server. pub errors: Option>, + /// Additional extensions. Their exact format is defined by the server. + /// See [GraphQL Response Specification](https://github.com/graphql/graphql-spec/blob/main/spec/Section%207%20--%20Response.md#response-format) + pub extensions: Option>, +} + +/// Hidden module for types used by the codegen crate. +#[doc(hidden)] +pub mod _private { + pub use ::serde; } #[cfg(test)] diff --git a/graphql_client/src/reqwest.rs b/graphql_client/src/reqwest.rs new file mode 100644 index 000000000..3541915e9 --- /dev/null +++ b/graphql_client/src/reqwest.rs @@ -0,0 +1,30 @@ +//! A concrete client implementation over HTTP with reqwest. + +use crate::GraphQLQuery; +use reqwest_crate as reqwest; + +/// Use the provided reqwest::Client to post a GraphQL request. +#[cfg(any(feature = "reqwest", feature = "reqwest-rustls"))] +pub async fn post_graphql( + client: &reqwest::Client, + url: U, + variables: Q::Variables, +) -> Result, reqwest::Error> { + let body = Q::build_query(variables); + let reqwest_response = client.post(url).json(&body).send().await?; + + reqwest_response.json().await +} + +/// Use the provided reqwest::Client to post a GraphQL request. +#[cfg(feature = "reqwest-blocking")] +pub fn post_graphql_blocking( + client: &reqwest::blocking::Client, + url: U, + variables: Q::Variables, +) -> Result, reqwest::Error> { + let body = Q::build_query(variables); + let reqwest_response = client.post(url).json(&body).send()?; + + reqwest_response.json() +} diff --git a/graphql_client/src/serde_with.rs b/graphql_client/src/serde_with.rs new file mode 100644 index 000000000..dcd5cd980 --- /dev/null +++ b/graphql_client/src/serde_with.rs @@ -0,0 +1,41 @@ +//! Helpers for overriding default serde implementations. + +use serde::{Deserialize, Deserializer}; + +#[derive(Deserialize)] +#[serde(untagged)] +enum IntOrString { + Int(i64), + Str(String), +} + +impl From for String { + fn from(value: IntOrString) -> Self { + match value { + IntOrString::Int(n) => n.to_string(), + IntOrString::Str(s) => s, + } + } +} + +/// Deserialize an optional ID type from either a String or an Integer representation. +/// +/// This is used by the codegen to enable String IDs to be deserialized from +/// either Strings or Integers. +pub fn deserialize_option_id<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + Option::::deserialize(deserializer).map(|opt| opt.map(String::from)) +} + +/// Deserialize an ID type from either a String or an Integer representation. +/// +/// This is used by the codegen to enable String IDs to be deserialized from +/// either Strings or Integers. +pub fn deserialize_id<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + IntOrString::deserialize(deserializer).map(String::from) +} diff --git a/graphql_client/src/web.rs b/graphql_client/src/web.rs deleted file mode 100644 index 00a6a3521..000000000 --- a/graphql_client/src/web.rs +++ /dev/null @@ -1,155 +0,0 @@ -//! Use graphql_client inside browsers with -//! [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen). - -use crate::*; -use failure::*; -use futures::{Future, IntoFuture}; -use log::*; -use std::collections::HashMap; -use wasm_bindgen::{JsCast, JsValue}; -use wasm_bindgen_futures::JsFuture; - -/// The main interface to the library. -/// -/// The workflow is the following: -/// -/// - create a client -/// - (optionally) configure it -/// - use it to perform queries with the [call] method -pub struct Client { - endpoint: String, - headers: HashMap, -} - -/// All the ways a request can go wrong. -/// -/// not exhaustive -#[derive(Debug, Fail, PartialEq)] -pub enum ClientError { - /// The body couldn't be built - #[fail(display = "Request body is not a valid string")] - Body, - /// An error caused by window.fetch - #[fail(display = "Network error")] - Network(String), - /// Error in a dynamic JS cast that should have worked - #[fail(display = "JS casting error")] - Cast, - /// No window object could be retrieved - #[fail( - display = "No Window object available - the client works only in a browser (non-worker) context" - )] - NoWindow, - /// Response shape does not match the generated code - #[fail(display = "Response shape error")] - ResponseShape, - /// Response could not be converted to text - #[fail(display = "Response conversion to text failed (Response.text threw)")] - ResponseText, - /// Exception thrown when building the request - #[fail(display = "Error building the request")] - RequestError, - /// Other JS exception - #[fail(display = "Unexpected JS exception")] - JsException, -} - -impl Client { - /// Initialize a client. The `endpoint` parameter is the URI of the GraphQL API. - pub fn new(endpoint: Endpoint) -> Client - where - Endpoint: Into, - { - Client { - endpoint: endpoint.into(), - headers: HashMap::new(), - } - } - - /// Add a header to those sent with the requests. Can be used for things like authorization. - pub fn add_header(&mut self, name: &str, value: &str) { - self.headers.insert(name.into(), value.into()); - } - - /// Perform a query. - /// - // Lint disabled: We can pass by value because it's always an empty struct. - #[allow(clippy::needless_pass_by_value)] - pub fn call( - &self, - _query: Q, - variables: Q::Variables, - ) -> impl Future, Error = ClientError> + 'static { - // this can be removed when we convert to async/await - let endpoint = self.endpoint.clone(); - let custom_headers = self.headers.clone(); - - web_sys::window() - .ok_or_else(|| ClientError::NoWindow) - .into_future() - .and_then(move |window| { - serde_json::to_string(&Q::build_query(variables)) - .map_err(|_| ClientError::Body) - .map(move |body| (window, body)) - }) - .and_then(move |(window, body)| { - let mut request_init = web_sys::RequestInit::new(); - request_init - .method("POST") - .body(Some(&JsValue::from_str(&body))); - - web_sys::Request::new_with_str_and_init(&endpoint, &request_init) - .map_err(|_| ClientError::JsException) - .map(|request| (window, request)) - // "Request constructor threw"); - }) - .and_then(move |(window, request)| { - let headers = request.headers(); - headers - .set("Content-Type", "application/json") - .map_err(|_| ClientError::RequestError)?; - headers - .set("Accept", "application/json") - .map_err(|_| ClientError::RequestError)?; - - for (header_name, header_value) in custom_headers.iter() { - headers - .set(header_name, header_value) - .map_err(|_| ClientError::RequestError)?; - } - - Ok((window, request)) - }) - .and_then(move |(window, request)| { - JsFuture::from(window.fetch_with_request(&request)) - .map_err(|err| ClientError::Network(js_sys::Error::from(err).message().into())) - }) - .and_then(move |res| { - debug!("response: {:?}", res); - res.dyn_into::() - .map_err(|_| ClientError::Cast) - }) - .and_then(move |cast_response| { - cast_response.text().map_err(|_| ClientError::ResponseText) - }) - .and_then(move |text_promise| { - JsFuture::from(text_promise).map_err(|_| ClientError::ResponseText) - }) - .and_then(|text| { - let response_text = text.as_string().unwrap_or_default(); - debug!("response text as string: {:?}", response_text); - serde_json::from_str(&response_text).map_err(|_| ClientError::ResponseShape) - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn client_new() { - Client::new("https://example.com/graphql"); - Client::new("/graphql"); - } -} diff --git a/graphql_client/tests/Germany.graphql b/graphql_client/tests/Germany.graphql index 322d4f391..d1321dddd 100644 --- a/graphql_client/tests/Germany.graphql +++ b/graphql_client/tests/Germany.graphql @@ -7,7 +7,7 @@ query Germany { } } -query Country($countryCode: String!) { +query Country($countryCode: ID!) { country(code: $countryCode) { name continent { diff --git a/graphql_client/tests/default.rs b/graphql_client/tests/default.rs new file mode 100644 index 000000000..3c105cb22 --- /dev/null +++ b/graphql_client/tests/default.rs @@ -0,0 +1,14 @@ +use graphql_client::*; + +#[derive(GraphQLQuery)] +#[graphql( + query_path = "tests/default/query.graphql", + schema_path = "tests/default/schema.graphql", + variables_derives = "Default" +)] +struct OptQuery; + +#[test] +fn variables_can_derive_default() { + let _: ::Variables = Default::default(); +} diff --git a/graphql_client/tests/default/query.graphql b/graphql_client/tests/default/query.graphql new file mode 100644 index 000000000..bd2fbab79 --- /dev/null +++ b/graphql_client/tests/default/query.graphql @@ -0,0 +1,6 @@ +query OptQuery($param: Param) { + optInput(query: $param) { + name + __typename + } +} diff --git a/graphql_client/tests/default/schema.graphql b/graphql_client/tests/default/schema.graphql new file mode 100644 index 000000000..cc803018b --- /dev/null +++ b/graphql_client/tests/default/schema.graphql @@ -0,0 +1,21 @@ +schema { + query: Query +} + +# The query type, represents all of the entry points into our object graph +type Query { + optInput(query: Param): Named +} + +# What can be searched for. +enum Param { + AUTHOR +} + +# A named entity +type Named { + # The ID of the entity + id: ID! + # The name of the entity + name: String! +} diff --git a/graphql_client/tests/extern_enums.rs b/graphql_client/tests/extern_enums.rs new file mode 100644 index 000000000..7a4accc25 --- /dev/null +++ b/graphql_client/tests/extern_enums.rs @@ -0,0 +1,83 @@ +use graphql_client::*; +use serde::Deserialize; + +/* + * Enums under test + * + * They rename the fields to use SCREAMING_SNAKE_CASE for deserialization, as it is the standard for GraphQL enums. + */ +#[derive(Deserialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum Direction { + North, + East, + South, + West, +} + +#[derive(Deserialize, Debug, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DistanceUnit { + Meter, + Feet, + SomethingElseWithMultipleWords, +} + +/* Queries */ + +// Minimal setup using extern enum. +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "tests/extern_enums/schema.graphql", + query_path = "tests/extern_enums/single_extern_enum_query.graphql", + extern_enums("DistanceUnit") +)] +pub struct SingleExternEnumQuery; + +// Tests using multiple externally defined enums. Also covers mixing with derived traits and with nullable GraphQL enum values. +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "tests/extern_enums/schema.graphql", + query_path = "tests/extern_enums/multiple_extern_enums_query.graphql", + response_derives = "Debug, PartialEq, Eq", + extern_enums("Direction", "DistanceUnit") +)] +pub struct MultipleExternEnumsQuery; + +/* Tests */ + +#[test] +fn single_extern_enum() { + const RESPONSE: &str = include_str!("extern_enums/single_extern_enum_response.json"); + + println!("{:?}", RESPONSE); + let response_data: single_extern_enum_query::ResponseData = + serde_json::from_str(RESPONSE).unwrap(); + + println!("{:?}", response_data.unit); + + let expected = single_extern_enum_query::ResponseData { + unit: DistanceUnit::Meter, + }; + + assert_eq!(response_data.unit, expected.unit); +} + +#[test] +fn multiple_extern_enums() { + const RESPONSE: &str = include_str!("extern_enums/multiple_extern_enums_response.json"); + + println!("{:?}", RESPONSE); + let response_data: multiple_extern_enums_query::ResponseData = + serde_json::from_str(RESPONSE).unwrap(); + + println!("{:?}", response_data); + + let expected = multiple_extern_enums_query::ResponseData { + distance: 100, + direction: Some(Direction::North), + unit: DistanceUnit::SomethingElseWithMultipleWords, + }; + + assert_eq!(response_data, expected); +} diff --git a/graphql_client/tests/extern_enums/multiple_extern_enums_query.graphql b/graphql_client/tests/extern_enums/multiple_extern_enums_query.graphql new file mode 100644 index 000000000..98295b586 --- /dev/null +++ b/graphql_client/tests/extern_enums/multiple_extern_enums_query.graphql @@ -0,0 +1,5 @@ +query MultipleExternEnumsQuery { + distance + unit + direction +} diff --git a/graphql_client/tests/extern_enums/multiple_extern_enums_response.json b/graphql_client/tests/extern_enums/multiple_extern_enums_response.json new file mode 100644 index 000000000..7ea9ab213 --- /dev/null +++ b/graphql_client/tests/extern_enums/multiple_extern_enums_response.json @@ -0,0 +1,5 @@ +{ + "distance": 100, + "unit": "SOMETHING_ELSE_WITH_MULTIPLE_WORDS", + "direction": "NORTH" +} diff --git a/graphql_client/tests/extern_enums/schema.graphql b/graphql_client/tests/extern_enums/schema.graphql new file mode 100644 index 000000000..2fcff6627 --- /dev/null +++ b/graphql_client/tests/extern_enums/schema.graphql @@ -0,0 +1,22 @@ +schema { + query: ExternEnumQueryRoot +} + +enum Direction { + NORTH + EAST + SOUTH + WEST +} + +enum DistanceUnit { + METER + FEET + SOMETHING_ELSE_WITH_MULTIPLE_WORDS +} + +type ExternEnumQueryRoot { + distance: Int! + unit: DistanceUnit! + direction: Direction +} diff --git a/graphql_client/tests/extern_enums/single_extern_enum_query.graphql b/graphql_client/tests/extern_enums/single_extern_enum_query.graphql new file mode 100644 index 000000000..eae9da3f8 --- /dev/null +++ b/graphql_client/tests/extern_enums/single_extern_enum_query.graphql @@ -0,0 +1,3 @@ +query SingleExternEnumQuery { + unit +} diff --git a/graphql_client/tests/extern_enums/single_extern_enum_response.json b/graphql_client/tests/extern_enums/single_extern_enum_response.json new file mode 100644 index 000000000..f563b4019 --- /dev/null +++ b/graphql_client/tests/extern_enums/single_extern_enum_response.json @@ -0,0 +1,3 @@ +{ + "unit": "METER" +} diff --git a/graphql_client/tests/fragment_chain.rs b/graphql_client/tests/fragment_chain.rs new file mode 100644 index 000000000..40c1e6fc8 --- /dev/null +++ b/graphql_client/tests/fragment_chain.rs @@ -0,0 +1,9 @@ +use graphql_client::*; + +#[allow(dead_code)] +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "tests/fragment_chain/schema.graphql", + query_path = "tests/fragment_chain/query.graphql" +)] +struct Q; diff --git a/graphql_client/tests/fragment_chain/query.graphql b/graphql_client/tests/fragment_chain/query.graphql new file mode 100644 index 000000000..402d4d30a --- /dev/null +++ b/graphql_client/tests/fragment_chain/query.graphql @@ -0,0 +1,11 @@ +query Q { + ...FragmentB +} + +fragment FragmentB on Query { + ...FragmentA +} + +fragment FragmentA on Query { + x +} diff --git a/graphql_client/tests/fragment_chain/schema.graphql b/graphql_client/tests/fragment_chain/schema.graphql new file mode 100644 index 000000000..c54fb81e6 --- /dev/null +++ b/graphql_client/tests/fragment_chain/schema.graphql @@ -0,0 +1,3 @@ +type Query { + x: String +} diff --git a/graphql_client/tests/fragments.rs b/graphql_client/tests/fragments.rs index 28572b0e7..902ec409e 100644 --- a/graphql_client/tests/fragments.rs +++ b/graphql_client/tests/fragments.rs @@ -24,13 +24,7 @@ fn fragment_reference() { let valid_fragment_reference = serde_json::from_value::(valid_response).unwrap(); - assert_eq!( - valid_fragment_reference - .fragment_reference - .in_fragment - .unwrap(), - "value" - ); + assert_eq!(valid_fragment_reference.in_fragment.unwrap(), "value"); } #[test] @@ -42,13 +36,7 @@ fn fragments_with_snake_case_name() { let valid_fragment_reference = serde_json::from_value::(valid_response).unwrap(); - assert_eq!( - valid_fragment_reference - .snake_case_fragment - .in_fragment - .unwrap(), - "value" - ); + assert_eq!(valid_fragment_reference.in_fragment.unwrap(), "value"); } #[derive(GraphQLQuery)] @@ -64,11 +52,9 @@ fn recursive_fragment() { let _ = RecursiveFragment { head: Some("ABCD".to_string()), - tail: Some(RecursiveFragmentTail { - recursive_fragment: Box::new(RecursiveFragment { - head: Some("EFGH".to_string()), - tail: None, - }), - }), + tail: Some(Box::new(RecursiveFragment { + head: Some("EFGH".to_string()), + tail: None, + })), }; } diff --git a/graphql_client/tests/input_object_variables.rs b/graphql_client/tests/input_object_variables.rs index c31826ac6..621ca824e 100644 --- a/graphql_client/tests/input_object_variables.rs +++ b/graphql_client/tests/input_object_variables.rs @@ -29,7 +29,7 @@ type Email = String; #[graphql( query_path = "tests/input_object_variables/input_object_variables_query_defaults.graphql", schema_path = "tests/input_object_variables/input_object_variables_schema.graphql", - response_derives = "Debug, PartialEq" + response_derives = "Debug, PartialEq, Eq" )] pub struct DefaultInputObjectVariablesQuery; @@ -39,16 +39,20 @@ fn input_object_variables_default() { msg: default_input_object_variables_query::Variables::default_msg(), }; - let out = serde_json::to_string(&variables).unwrap(); + let out = serde_json::to_value(variables).unwrap(); - assert_eq!(out, r#"{"msg":{"content":null,"to":{"category":null,"email":"rosa.luxemburg@example.com","name":null}}}"#); + let expected_default = serde_json::json!({ + "msg":{"content":null,"to":{"category":null,"email":"rosa.luxemburg@example.com","name":null}} + }); + + assert_eq!(out, expected_default); } #[derive(GraphQLQuery)] #[graphql( query_path = "tests/input_object_variables/input_object_variables_query.graphql", schema_path = "tests/input_object_variables/input_object_variables_schema.graphql", - response_derives = "Debug, PartialEq" + response_derives = "Debug, PartialEq, Eq" )] pub struct RecursiveInputQuery; @@ -56,12 +60,12 @@ pub struct RecursiveInputQuery; fn recursive_input_objects_can_be_constructed() { use recursive_input_query::*; - RecursiveInput { + let _ = RecursiveInput { head: "hello".to_string(), tail: Box::new(None), }; - RecursiveInput { + let _ = RecursiveInput { head: "hi".to_string(), tail: Box::new(Some(RecursiveInput { head: "this is crazy".to_string(), @@ -74,7 +78,25 @@ fn recursive_input_objects_can_be_constructed() { #[graphql( query_path = "tests/input_object_variables/input_object_variables_query.graphql", schema_path = "tests/input_object_variables/input_object_variables_schema.graphql", - response_derives = "Debug, PartialEq" + response_derives = "Debug, PartialEq, Eq" +)] +pub struct InputCaseTestsQuery; + +#[test] +fn input_objects_are_all_snake_case() { + use input_case_tests_query::*; + + let _ = CaseTestInput { + field_with_snake_case: "hello from".to_string(), + other_field_with_camel_case: "the other side".to_string(), + }; +} + +#[derive(GraphQLQuery)] +#[graphql( + query_path = "tests/input_object_variables/input_object_variables_query.graphql", + schema_path = "tests/input_object_variables/input_object_variables_schema.graphql", + response_derives = "Debug, PartialEq, Eq" )] pub struct IndirectlyRecursiveInputQuery; @@ -82,12 +104,12 @@ pub struct IndirectlyRecursiveInputQuery; fn indirectly_recursive_input_objects_can_be_constructed() { use indirectly_recursive_input_query::*; - IndirectlyRecursiveInput { + let _ = IndirectlyRecursiveInput { head: "hello".to_string(), tail: Box::new(None), }; - IndirectlyRecursiveInput { + let _ = IndirectlyRecursiveInput { head: "hi".to_string(), tail: Box::new(Some(IndirectlyRecursiveInputTailPart { name: "this is crazy".to_string(), @@ -95,3 +117,32 @@ fn indirectly_recursive_input_objects_can_be_constructed() { })), }; } + +#[derive(GraphQLQuery)] +#[graphql( + query_path = "tests/input_object_variables/input_object_variables_query.graphql", + schema_path = "tests/input_object_variables/input_object_variables_schema.graphql", + variables_derives = "Default", + response_derives = "Debug, PartialEq, Eq" +)] +pub struct RustNameQuery; + +#[test] +fn rust_name_correctly_mapped() { + use rust_name_query::*; + let value = serde_json::to_value(Variables { + extern_: Some("hello".to_owned()), + msg: <_>::default(), + }) + .unwrap(); + assert_eq!( + value + .as_object() + .unwrap() + .get("extern") + .unwrap() + .as_str() + .unwrap(), + "hello" + ); +} diff --git a/graphql_client/tests/input_object_variables/input_object_variables_query.graphql b/graphql_client/tests/input_object_variables/input_object_variables_query.graphql index 558e16d8f..e5ed9acea 100644 --- a/graphql_client/tests/input_object_variables/input_object_variables_query.graphql +++ b/graphql_client/tests/input_object_variables/input_object_variables_query.graphql @@ -11,3 +11,13 @@ query RecursiveInputQuery($input: RecursiveInput!) { query IndirectlyRecursiveInputQuery($input: IndirectlyRecursiveInput!) { saveRecursiveInput(recursiveInput: $input) } + +query InputCaseTestsQuery($input: CaseTestInput!) { + testQueryCase(caseTestInput: $input) +} + +query RustNameQuery($msg: Message, $extern: String) { + echo(message: $msg, extern: $extern) { + result + } +} diff --git a/graphql_client/tests/input_object_variables/input_object_variables_schema.graphql b/graphql_client/tests/input_object_variables/input_object_variables_schema.graphql index 34d7f1960..d6a7f28e6 100644 --- a/graphql_client/tests/input_object_variables/input_object_variables_schema.graphql +++ b/graphql_client/tests/input_object_variables/input_object_variables_schema.graphql @@ -39,8 +39,22 @@ input IndirectlyRecursiveInputTailPart { recursed_field: IndirectlyRecursiveInput } +input CaseTestInput { + field_with_snake_case: String! + otherFieldWithCamelCase: String! +} + +type CaseTestResult { + result: String! +} + type InputObjectVariablesQuery { - echo(message: Message!, options: Options = { pgpSignature: true }): EchoResult + echo( + message: Message! + options: Options = { pgpSignature: true } + extern: String = "" + ): EchoResult + testQueryCase(caseTestInput: CaseTestInput!): CaseTestResult saveRecursiveInput(recursiveInput: RecursiveInput!): Category } diff --git a/graphql_client/tests/int_id.rs b/graphql_client/tests/int_id.rs new file mode 100644 index 000000000..2291c5c3b --- /dev/null +++ b/graphql_client/tests/int_id.rs @@ -0,0 +1,41 @@ +use graphql_client::*; +use serde_json::json; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "tests/more_derives/schema.graphql", + query_path = "tests/more_derives/query.graphql", + response_derives = "Debug, PartialEq, Eq, std::cmp::PartialOrd" +)] +pub struct MoreDerives; + +#[test] +fn int_id() { + let response1 = json!({ + "currentUser": { + "id": 1, + "name": "Don Draper", + } + }); + + let response2 = json!({ + "currentUser": { + "id": "2", + "name": "Peggy Olson", + } + }); + + let res1 = serde_json::from_value::(response1) + .expect("should deserialize"); + assert_eq!( + res1.current_user.expect("res1 current user").id, + Some("1".into()) + ); + + let res2 = serde_json::from_value::(response2) + .expect("should deserialize"); + assert_eq!( + res2.current_user.expect("res2 current user").id, + Some("2".into()) + ); +} diff --git a/graphql_client/tests/int_id/query.graphql b/graphql_client/tests/int_id/query.graphql new file mode 100644 index 000000000..d71c02ff8 --- /dev/null +++ b/graphql_client/tests/int_id/query.graphql @@ -0,0 +1,6 @@ +query MoreDerives { + currentUser { + name + id + } +} diff --git a/graphql_client/tests/int_id/schema.graphql b/graphql_client/tests/int_id/schema.graphql new file mode 100644 index 000000000..21c707e94 --- /dev/null +++ b/graphql_client/tests/int_id/schema.graphql @@ -0,0 +1,12 @@ +schema { + query: TestQuery +} + +type TestQuery { + currentUser: TestUser +} + +type TestUser { + name: String + id: ID +} diff --git a/graphql_client/tests/interfaces.rs b/graphql_client/tests/interfaces.rs index 7bb97f511..6fc2e134b 100644 --- a/graphql_client/tests/interfaces.rs +++ b/graphql_client/tests/interfaces.rs @@ -1,12 +1,12 @@ use graphql_client::*; -const RESPONSE: &'static str = include_str!("interfaces/interface_response.json"); +const RESPONSE: &str = include_str!("interfaces/interface_response.json"); #[derive(GraphQLQuery)] #[graphql( query_path = "tests/interfaces/interface_query.graphql", schema_path = "tests/interfaces/interface_schema.graphql", - response_derives = "Debug, PartialEq" + response_derives = "Debug, PartialEq, Eq" )] pub struct InterfaceQuery; @@ -57,11 +57,11 @@ fn interface_deserialization() { #[graphql( query_path = "tests/interfaces/interface_not_on_everything_query.graphql", schema_path = "tests/interfaces/interface_schema.graphql", - response_derives = "Debug,PartialEq" + response_derives = "Debug, PartialEq, Eq" )] pub struct InterfaceNotOnEverythingQuery; -const RESPONSE_NOT_ON_EVERYTHING: &'static str = +const RESPONSE_NOT_ON_EVERYTHING: &str = include_str!("interfaces/interface_response_not_on_everything.json"); #[test] @@ -111,12 +111,11 @@ fn interface_not_on_everything_deserialization() { #[graphql( query_path = "tests/interfaces/interface_with_fragment_query.graphql", schema_path = "tests/interfaces/interface_schema.graphql", - response_derives = "Debug,PartialEq" + response_derives = "Debug, PartialEq, Eq" )] pub struct InterfaceWithFragmentQuery; -const RESPONSE_FRAGMENT: &'static str = - include_str!("interfaces/interface_with_fragment_response.json"); +const RESPONSE_FRAGMENT: &str = include_str!("interfaces/interface_with_fragment_response.json"); #[test] fn fragment_in_interface() { diff --git a/graphql_client/tests/interfaces/interface_with_type_refining_fragment_query.graphql b/graphql_client/tests/interfaces/interface_with_type_refining_fragment_query.graphql index ec364f4fb..471177ed9 100644 --- a/graphql_client/tests/interfaces/interface_with_type_refining_fragment_query.graphql +++ b/graphql_client/tests/interfaces/interface_with_type_refining_fragment_query.graphql @@ -1,4 +1,4 @@ -fragment Birthday on Person { +fragment BirthdayFragment on Person { birthday } @@ -9,7 +9,7 @@ query QueryOnInterface { ... on Dog { isGoodDog } - ...Birthday + ...BirthdayFragment ... on Organization { industry } diff --git a/graphql_client/tests/introspection.rs b/graphql_client/tests/introspection.rs index a261c2f25..acb5e9dd4 100644 --- a/graphql_client/tests/introspection.rs +++ b/graphql_client/tests/introspection.rs @@ -4,7 +4,7 @@ use graphql_client::*; #[graphql( query_path = "tests/introspection/introspection_query.graphql", schema_path = "tests/introspection/introspection_schema.graphql", - response_derives = "Debug,PartialEq" + response_derives = "Debug, PartialEq, Eq" )] pub struct IntrospectionQuery; diff --git a/graphql_client/tests/json_schema.rs b/graphql_client/tests/json_schema.rs index 0da662570..86e2298fe 100644 --- a/graphql_client/tests/json_schema.rs +++ b/graphql_client/tests/json_schema.rs @@ -7,7 +7,7 @@ type Uuid = String; #[graphql( query_path = "tests/json_schema/query.graphql", schema_path = "tests/json_schema/schema_1.json", - response_derives = "Debug,PartialEq" + response_derives = "Debug, PartialEq, Eq" )] pub struct WithSchema1; diff --git a/graphql_client/tests/more_derives.rs b/graphql_client/tests/more_derives.rs index f1ed1d7be..46be3fa74 100644 --- a/graphql_client/tests/more_derives.rs +++ b/graphql_client/tests/more_derives.rs @@ -4,7 +4,7 @@ use graphql_client::*; #[graphql( schema_path = "tests/more_derives/schema.graphql", query_path = "tests/more_derives/query.graphql", - response_derives = "Debug, PartialEq, PartialOrd" + response_derives = "Debug, PartialEq, Eq, std::cmp::PartialOrd" )] pub struct MoreDerives; diff --git a/graphql_client/tests/one_of_input.rs b/graphql_client/tests/one_of_input.rs new file mode 100644 index 000000000..80ebde3c7 --- /dev/null +++ b/graphql_client/tests/one_of_input.rs @@ -0,0 +1,30 @@ +use graphql_client::*; +use serde_json::*; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "tests/one_of_input/schema.graphql", + query_path = "tests/one_of_input/query.graphql", + variables_derives = "Clone" +)] +pub struct OneOfMutation; + +#[test] +fn one_of_input() { + use one_of_mutation::*; + + let author = Param::Author(Author { id: 1 }); + let _ = Param::Name("Mark Twain".to_string()); + let _ = Param::RecursiveDirect(Box::new(author.clone())); + let _ = Param::RecursiveIndirect(Box::new(Recursive { + param: Box::new(author.clone()), + })); + let _ = Param::RequiredInts(vec![1]); + let _ = Param::OptionalInts(vec![Some(1)]); + + let query = OneOfMutation::build_query(Variables { param: author }); + assert_eq!( + json!({ "param": { "author":{ "id": 1 } } }), + serde_json::to_value(&query.variables).expect("json"), + ); +} diff --git a/graphql_client/tests/one_of_input/query.graphql b/graphql_client/tests/one_of_input/query.graphql new file mode 100644 index 000000000..52f167eee --- /dev/null +++ b/graphql_client/tests/one_of_input/query.graphql @@ -0,0 +1,3 @@ +mutation OneOfMutation($param: Param!) { + oneOfMutation(query: $param) +} diff --git a/graphql_client/tests/one_of_input/schema.graphql b/graphql_client/tests/one_of_input/schema.graphql new file mode 100644 index 000000000..d2a0e8754 --- /dev/null +++ b/graphql_client/tests/one_of_input/schema.graphql @@ -0,0 +1,24 @@ +schema { + mutation: Mutation +} + +type Mutation { + oneOfMutation(mutation: Param!): Int +} + +input Param @oneOf { + author: Author + name: String + recursiveDirect: Param + recursiveIndirect: Recursive + requiredInts: [Int!] + optionalInts: [Int] +} + +input Author { + id: Int! +} + +input Recursive { + param: Param! +} diff --git a/graphql_client/tests/operation_selection.rs b/graphql_client/tests/operation_selection.rs index 52ef6ef24..3e921a784 100644 --- a/graphql_client/tests/operation_selection.rs +++ b/graphql_client/tests/operation_selection.rs @@ -4,7 +4,7 @@ use graphql_client::GraphQLQuery; #[graphql( query_path = "tests/operation_selection/queries.graphql", schema_path = "tests/operation_selection/schema.graphql", - response_derives = "Debug,PartialEq" + response_derives = "Debug, PartialEq, Eq" )] pub struct Heights; @@ -12,12 +12,12 @@ pub struct Heights; #[graphql( query_path = "tests/operation_selection/queries.graphql", schema_path = "tests/operation_selection/schema.graphql", - response_derives = "Debug,PartialEq" + response_derives = "Debug, PartialEq, Eq" )] pub struct Echo; -const HEIGHTS_RESPONSE: &str = r##"{"mountainHeight": 224, "buildingHeight": 12}"##; -const ECHO_RESPONSE: &str = r##"{"echo": "tiramisù"}"##; +const HEIGHTS_RESPONSE: &str = r#"{"mountainHeight": 224, "buildingHeight": 12}"#; +const ECHO_RESPONSE: &str = r#"{"echo": "tiramisù"}"#; #[test] fn operation_selection_works() { diff --git a/graphql_client/tests/skip_serializing_none.rs b/graphql_client/tests/skip_serializing_none.rs new file mode 100644 index 000000000..bf177da4e --- /dev/null +++ b/graphql_client/tests/skip_serializing_none.rs @@ -0,0 +1,57 @@ +use graphql_client::*; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "tests/skip_serializing_none/schema.graphql", + query_path = "tests/skip_serializing_none/query.graphql", + skip_serializing_none +)] +pub struct SkipSerializingNoneMutation; + +#[test] +fn skip_serializing_none() { + use skip_serializing_none_mutation::*; + + let query = SkipSerializingNoneMutation::build_query(Variables { + optional_int: None, + optional_list: None, + non_optional_int: 1337, + non_optional_list: vec![], + param: Some(Param { + data: Author { + name: "test".to_owned(), + id: None, + }, + }), + }); + + let stringified = serde_json::to_string(&query).expect("SkipSerializingNoneMutation is valid"); + + println!("{}", stringified); + + assert!(stringified.contains(r#""param":{"data":{"name":"test"}}"#)); + assert!(stringified.contains(r#""nonOptionalInt":1337"#)); + assert!(stringified.contains(r#""nonOptionalList":[]"#)); + assert!(!stringified.contains(r#""optionalInt""#)); + assert!(!stringified.contains(r#""optionalList""#)); + + let query = SkipSerializingNoneMutation::build_query(Variables { + optional_int: Some(42), + optional_list: Some(vec![]), + non_optional_int: 1337, + non_optional_list: vec![], + param: Some(Param { + data: Author { + name: "test".to_owned(), + id: None, + }, + }), + }); + let stringified = serde_json::to_string(&query).expect("SkipSerializingNoneMutation is valid"); + println!("{}", stringified); + assert!(stringified.contains(r#""param":{"data":{"name":"test"}}"#)); + assert!(stringified.contains(r#""nonOptionalInt":1337"#)); + assert!(stringified.contains(r#""nonOptionalList":[]"#)); + assert!(stringified.contains(r#""optionalInt":42"#)); + assert!(stringified.contains(r#""optionalList":[]"#)); +} diff --git a/graphql_client/tests/skip_serializing_none/query.graphql b/graphql_client/tests/skip_serializing_none/query.graphql new file mode 100644 index 000000000..a91b798f4 --- /dev/null +++ b/graphql_client/tests/skip_serializing_none/query.graphql @@ -0,0 +1,6 @@ +mutation SkipSerializingNoneMutation($param: Param, $optionalInt: Int, $optionalList: [Int!], $nonOptionalInt: Int!, $nonOptionalList: [Int!]!) { + optInput(query: $param) { + name + __typename + } +} diff --git a/graphql_client/tests/skip_serializing_none/schema.graphql b/graphql_client/tests/skip_serializing_none/schema.graphql new file mode 100644 index 000000000..7314fa3c1 --- /dev/null +++ b/graphql_client/tests/skip_serializing_none/schema.graphql @@ -0,0 +1,25 @@ +schema { + mutation: Mutation +} + +# The query type, represents all of the entry points into our object graph +type Mutation { + optInput(mutation: Param!): Named +} + +input Param { + data: Author! +} + +input Author { + id: String + name: String! +} + +# A named entity +type Named { + # The ID of the entity + id: ID! + # The name of the entity + name: String! +} diff --git a/graphql_client/tests/subscriptions.rs b/graphql_client/tests/subscriptions.rs index 4335a9d7f..768aa6114 100644 --- a/graphql_client/tests/subscriptions.rs +++ b/graphql_client/tests/subscriptions.rs @@ -15,7 +15,7 @@ const RESPONSE: &str = include_str!("subscription/subscription_query_response.js #[graphql( schema_path = "tests/subscription/subscription_schema.graphql", query_path = "tests/subscription/subscription_query.graphql", - response_derives = "Debug, PartialEq" + response_derives = "Debug, PartialEq, Eq" )] pub struct SubscriptionQuery; diff --git a/graphql_client/tests/type_refining_fragments.rs b/graphql_client/tests/type_refining_fragments.rs index 8ecae78c5..da37a08e9 100644 --- a/graphql_client/tests/type_refining_fragments.rs +++ b/graphql_client/tests/type_refining_fragments.rs @@ -4,7 +4,7 @@ use graphql_client::*; #[graphql( query_path = "tests/interfaces/interface_with_type_refining_fragment_query.graphql", schema_path = "tests/interfaces/interface_schema.graphql", - response_derives = "Debug, PartialEq" + response_derives = "Debug, PartialEq, Eq" )] pub struct QueryOnInterface; @@ -12,13 +12,13 @@ pub struct QueryOnInterface; #[graphql( query_path = "tests/unions/type_refining_fragment_on_union_query.graphql", schema_path = "tests/unions/union_schema.graphql", - response_derives = "PartialEq, Debug" + response_derives = "Debug, PartialEq, Eq" )] pub struct QueryOnUnion; #[test] fn type_refining_fragment_on_union() { - const RESPONSE: &'static str = include_str!("unions/union_query_response.json"); + const RESPONSE: &str = include_str!("unions/union_query_response.json"); let response_data: query_on_union::ResponseData = serde_json::from_str(RESPONSE).unwrap(); @@ -49,7 +49,7 @@ fn type_refining_fragment_on_union() { fn type_refining_fragment_on_interface() { use crate::query_on_interface::*; - const RESPONSE: &'static str = include_str!("interfaces/interface_response.json"); + const RESPONSE: &str = include_str!("interfaces/interface_response.json"); let response_data: query_on_interface::ResponseData = serde_json::from_str(RESPONSE).unwrap(); diff --git a/graphql_client/tests/union_query.rs b/graphql_client/tests/union_query.rs index ab1e3cfd6..f4484cbdd 100644 --- a/graphql_client/tests/union_query.rs +++ b/graphql_client/tests/union_query.rs @@ -1,12 +1,13 @@ use graphql_client::*; -const RESPONSE: &'static str = include_str!("unions/union_query_response.json"); +const RESPONSE: &str = include_str!("unions/union_query_response.json"); +const FRAGMENT_AND_MORE_RESPONSE: &str = include_str!("unions/fragment_and_more_response.json"); #[derive(GraphQLQuery)] #[graphql( query_path = "tests/unions/union_query.graphql", schema_path = "tests/unions/union_schema.graphql", - response_derives = "PartialEq, Debug" + response_derives = "Debug, PartialEq, Eq" )] pub struct UnionQuery; @@ -14,10 +15,18 @@ pub struct UnionQuery; #[graphql( query_path = "tests/unions/union_query.graphql", schema_path = "tests/unions/union_schema.graphql", - response_derives = "PartialEq, Debug" + response_derives = "Debug, PartialEq, Eq" )] pub struct FragmentOnUnion; +#[derive(GraphQLQuery)] +#[graphql( + query_path = "tests/unions/union_query.graphql", + schema_path = "tests/unions/union_schema.graphql", + response_derives = "Debug, PartialEq, Eq" +)] +pub struct FragmentAndMoreOnUnion; + #[test] fn union_query_deserialization() { let response_data: union_query::ResponseData = serde_json::from_str(RESPONSE).unwrap(); @@ -53,26 +62,63 @@ fn fragment_on_union() { let expected = fragment_on_union::ResponseData { names: Some(vec![ - fragment_on_union::FragmentOnUnionNames::Person( - fragment_on_union::FragmentOnUnionNamesOnPerson { - first_name: "Audrey".to_string(), - }, - ), - fragment_on_union::FragmentOnUnionNames::Dog( - fragment_on_union::FragmentOnUnionNamesOnDog { - name: "Laïka".to_string(), - }, - ), - fragment_on_union::FragmentOnUnionNames::Organization( - fragment_on_union::FragmentOnUnionNamesOnOrganization { + fragment_on_union::NamesFragment::Person(fragment_on_union::NamesFragmentOnPerson { + first_name: "Audrey".to_string(), + }), + fragment_on_union::NamesFragment::Dog(fragment_on_union::NamesFragmentOnDog { + name: "Laïka".to_string(), + }), + fragment_on_union::NamesFragment::Organization( + fragment_on_union::NamesFragmentOnOrganization { title: "Mozilla".to_string(), }, ), - fragment_on_union::FragmentOnUnionNames::Dog( - fragment_on_union::FragmentOnUnionNamesOnDog { - name: "Norbert".to_string(), - }, - ), + fragment_on_union::NamesFragment::Dog(fragment_on_union::NamesFragmentOnDog { + name: "Norbert".to_string(), + }), + ]), + }; + + assert_eq!(response_data, expected); +} + +#[test] +fn fragment_and_more_on_union() { + use fragment_and_more_on_union::*; + + let response_data: fragment_and_more_on_union::ResponseData = + serde_json::from_str(FRAGMENT_AND_MORE_RESPONSE).unwrap(); + + let expected = fragment_and_more_on_union::ResponseData { + names: Some(vec![ + FragmentAndMoreOnUnionNames { + names_fragment: NamesFragment::Person(NamesFragmentOnPerson { + first_name: "Larry".into(), + }), + on: FragmentAndMoreOnUnionNamesOn::Person, + }, + FragmentAndMoreOnUnionNames { + names_fragment: NamesFragment::Dog(NamesFragmentOnDog { + name: "Laïka".into(), + }), + on: FragmentAndMoreOnUnionNamesOn::Dog(FragmentAndMoreOnUnionNamesOnDog { + is_good_dog: true, + }), + }, + FragmentAndMoreOnUnionNames { + names_fragment: NamesFragment::Organization(NamesFragmentOnOrganization { + title: "Mozilla".into(), + }), + on: FragmentAndMoreOnUnionNamesOn::Organization, + }, + FragmentAndMoreOnUnionNames { + names_fragment: NamesFragment::Dog(NamesFragmentOnDog { + name: "Norbert".into(), + }), + on: FragmentAndMoreOnUnionNamesOn::Dog(FragmentAndMoreOnUnionNamesOnDog { + is_good_dog: true, + }), + }, ]), }; diff --git a/graphql_client/tests/unions/fragment_and_more_response.json b/graphql_client/tests/unions/fragment_and_more_response.json new file mode 100644 index 000000000..067cb0ab2 --- /dev/null +++ b/graphql_client/tests/unions/fragment_and_more_response.json @@ -0,0 +1,22 @@ +{ + "names": [ + { + "__typename": "Person", + "firstName": "Larry" + }, + { + "__typename": "Dog", + "name": "Laïka", + "isGoodDog": true + }, + { + "__typename": "Organization", + "title": "Mozilla" + }, + { + "__typename": "Dog", + "name": "Norbert", + "isGoodDog": true + } + ] +} diff --git a/graphql_client/tests/unions/union_query.graphql b/graphql_client/tests/unions/union_query.graphql index 28387924f..aec63d083 100644 --- a/graphql_client/tests/unions/union_query.graphql +++ b/graphql_client/tests/unions/union_query.graphql @@ -32,3 +32,12 @@ query FragmentOnUnion { ...NamesFragment } } + +query FragmentAndMoreOnUnion { + names { + ...NamesFragment + ... on Dog { + isGoodDog + } + } +} diff --git a/graphql_client/tests/web.rs b/graphql_client/tests/web.rs deleted file mode 100644 index 5a13b4340..000000000 --- a/graphql_client/tests/web.rs +++ /dev/null @@ -1,100 +0,0 @@ -#![cfg(target_arch = "wasm32")] - -use futures::Future; -use graphql_client::{web::Client, GraphQLQuery}; -use wasm_bindgen::JsValue; -use wasm_bindgen_test::wasm_bindgen_test_configure; -use wasm_bindgen_test::*; - -wasm_bindgen_test_configure!(run_in_browser); - -#[wasm_bindgen_test] -fn build_client() { - // just to test it doesn't crash - Client::new("https://example.com/graphql"); - Client::new("/graphql"); -} - -#[derive(GraphQLQuery)] -#[graphql( - schema_path = "tests/countries_schema.json", - query_path = "tests/Germany.graphql" -)] -struct Germany; - -#[wasm_bindgen_test(async)] -fn test_germany() -> impl Future { - Client::new("https://countries.trevorblades.com/") - .call(Germany, germany::Variables) - .map(|response| { - let continent_name = response - .data - .expect("response data is not null") - .country - .expect("country is not null") - .continent - .expect("continent is not null") - .name - .expect("germany is on a continent"); - - assert_eq!(continent_name, "Europe"); - }) - .map_err(|err| { - panic!("{:?}", err); - }) -} - -#[derive(GraphQLQuery)] -#[graphql( - schema_path = "tests/countries_schema.json", - query_path = "tests/Germany.graphql" -)] -struct Country; - -#[wasm_bindgen_test(async)] -fn test_country() -> impl Future { - Client::new("https://countries.trevorblades.com/") - .call( - Country, - country::Variables { - country_code: "CN".to_owned(), - }, - ) - .map(|response| { - let continent_name = response - .data - .expect("response data is not null") - .country - .expect("country is not null") - .continent - .expect("continent is not null") - .name - .expect("country is on a continent"); - - assert_eq!(continent_name, "Asia"); - }) - .map_err(|err| { - panic!("{:?}", err); - }) -} - -#[wasm_bindgen_test(async)] -fn test_bad_url() -> impl Future { - Client::new("https://example.com/non-existent/graphql/endpoint") - .call( - Country, - country::Variables { - country_code: "CN".to_owned(), - }, - ) - .map(|_response| panic!("The API endpoint does not exist, this should not be called.")) - .map_err(|err| { - assert_eq!( - err, - graphql_client::web::ClientError::Network( - "NetworkError when attempting to fetch resource.".into() - ) - ); - }) - .then(|_| Ok(())) -} diff --git a/graphql_client_cli/Cargo.toml b/graphql_client_cli/Cargo.toml index 8ffa2716e..924b374ff 100644 --- a/graphql_client_cli/Cargo.toml +++ b/graphql_client_cli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "graphql_client_cli" description = "The CLI for graphql-client" -version = "0.8.0" +version = "0.14.0" authors = ["Tom Houlé "] license = "Apache-2.0 OR MIT" repository = "https://github.com/graphql-rust/graphql-client" @@ -12,19 +12,16 @@ name = "graphql-client" path = "src/main.rs" [dependencies] -failure = "^0.1" -reqwest = "^0.9" -graphql_client = { version = "0.8.0", path = "../graphql_client" } -graphql_client_codegen = { path = "../graphql_client_codegen/", version = "0.8.0" } -structopt = "0.2.18" +reqwest = { version = "0.12", features = ["json", "blocking"] } +graphql_client = { version = "0.14.0", path = "../graphql_client", default-features = false, features = ["graphql_query_derive", "reqwest-blocking"] } +graphql_client_codegen = { path = "../graphql_client_codegen/", version = "0.14.0" } +clap = { version = "^4.0", features = ["derive"] } serde = { version = "^1.0", features = ["derive"] } serde_json = "^1.0" -syn = "^1.0" log = "^0.4" -env_logger = "^0.6" - -rustfmt-nightly = { version = "1.4.5", optional = true } +env_logger = { version = "0.10.2", features = ["color"] } +syn = { version = "^2.0", features = ["full"] } +anstyle = "1.0.10" [features] default = [] -rustfmt = ["rustfmt-nightly"] diff --git a/graphql_client_cli/README.md b/graphql_client_cli/README.md index 4355d01f4..18aab172b 100644 --- a/graphql_client_cli/README.md +++ b/graphql_client_cli/README.md @@ -1,6 +1,6 @@ # GraphQL client CLI -This is still a WIP, the main use for it now is to download the `schema.json` from a GraphQL endpoint, which you can also do with [apollo-codegen](https://github.com/apollographql/apollo-cli). +This is still a WIP, the main use for it now is to download the `schema.json` from a GraphQL endpoint, which you can also do with [the Apollo CLI](https://github.com/apollographql/apollo-tooling#apollo-clientdownload-schema-output). ## Install @@ -14,14 +14,16 @@ cargo install graphql_client_cli --force Get the schema from a live GraphQL API. The schema is printed to stdout. USAGE: - graphql-client introspect-schema [OPTIONS] + graphql-client introspect-schema [FLAGS] [OPTIONS] FLAGS: -h, --help Prints help information -V, --version Prints version information + --no-ssl Set this option to disable ssl certificate verification. Default value is false. + ssl verification is turned on by default. OPTIONS: - --authorization Set the contents of the Authorizaiton header. + --authorization Set the contents of the Authorization header. --header ... Specify custom headers. --header 'X-Name: Value' --output Where to write the JSON for the introspected schema. @@ -33,35 +35,41 @@ ARGS: ``` USAGE: - graphql-client generate [FLAGS] [OPTIONS] + graphql-client generate [FLAGS] [OPTIONS] --schema-path FLAGS: -h, --help Prints help information --no-formatting If you don't want to execute rustfmt to generated code, set this option. Default value is - false. Formating feature is disabled as default installation. + false. -V, --version Prints version information OPTIONS: - -a, --additional-derives - Additional derives that will be added to the generated structs and enums for the response and the variables. - --additional-derives='Serialize,PartialEq' + -I, --variables-derives + Additional derives that will be added to the generated structs and enums for the variables. + --variables-derives='Serialize,PartialEq' + -O, --response-derives + Additional derives that will be added to the generated structs and enums for the response. + --response-derives='Serialize,PartialEq' -d, --deprecation-strategy You can choose deprecation strategy from allow, deny, or warn. Default value is warn. - -m, --module_visibility + -m, --module-visibility You can choose module and target struct visibility from pub and private. Default value is pub. + -o, --output-directory The directory in which the code will be generated + -s, --schema-path Path to GraphQL schema file (.json or .graphql). -o, --selected-operation Name of target query. If you don't set this parameter, cli generate all queries in query file. + --fragments-other-variant + Generate an Unknown variant for enums generated by fragments. ARGS: - Path to graphql query file. - Path to graphql schema file. + Path to the GraphQL query file. ``` If you want to use formatting feature, you should install like this. ```bash -cargo install graphql_client_cli --features rustfmt --force +cargo install graphql_client_cli ``` diff --git a/graphql_client_cli/src/error.rs b/graphql_client_cli/src/error.rs new file mode 100644 index 000000000..feb682c2f --- /dev/null +++ b/graphql_client_cli/src/error.rs @@ -0,0 +1,65 @@ +use std::fmt::{Debug, Display}; + +pub struct Error { + source: Option>, + message: Option, + location: &'static std::panic::Location<'static>, +} + +impl Error { + #[track_caller] + pub fn message(msg: String) -> Self { + Error { + source: None, + message: Some(msg), + location: std::panic::Location::caller(), + } + } + + #[track_caller] + pub fn source_with_message( + source: impl std::error::Error + Send + Sync + 'static, + message: String, + ) -> Self { + let mut err = Error::message(message); + err.source = Some(Box::new(source)); + err + } +} + +// This is the impl that shows up when the error bubbles up to `main()`. +impl Debug for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(msg) = &self.message { + f.write_str(msg)?; + f.write_str("\n")?; + } + + if self.source.is_some() && self.message.is_some() { + f.write_str("Cause: ")?; + } + + if let Some(source) = self.source.as_ref() { + Display::fmt(source, f)?; + } + + f.write_str("\nLocation: ")?; + Display::fmt(self.location, f)?; + + Ok(()) + } +} + +impl From for Error +where + T: std::error::Error + Send + Sync + 'static, +{ + #[track_caller] + fn from(err: T) -> Self { + Error { + message: None, + source: Some(Box::new(err)), + location: std::panic::Location::caller(), + } + } +} diff --git a/graphql_client_cli/src/generate.rs b/graphql_client_cli/src/generate.rs index dcd001df1..1a36d0cff 100644 --- a/graphql_client_cli/src/generate.rs +++ b/graphql_client_cli/src/generate.rs @@ -1,26 +1,38 @@ -use failure::*; +use crate::error::Error; +use crate::CliResult; use graphql_client_codegen::{ generate_module_token_stream, CodegenMode, GraphQLClientCodegenOptions, }; +use std::ffi::OsString; use std::fs::File; use std::io::Write as _; use std::path::PathBuf; -use syn::Token; +use std::process::Stdio; +use syn::{token::Paren, token::Pub, VisRestricted, Visibility}; pub(crate) struct CliCodegenParams { pub query_path: PathBuf, pub schema_path: PathBuf, pub selected_operation: Option, - pub additional_derives: Option, + pub variables_derives: Option, + pub response_derives: Option, pub deprecation_strategy: Option, pub no_formatting: bool, pub module_visibility: Option, pub output_directory: Option, + pub custom_scalars_module: Option, + pub fragments_other_variant: bool, + pub external_enums: Option>, + pub custom_variable_types: Option, + pub custom_response_type: Option, } -pub(crate) fn generate_code(params: CliCodegenParams) -> Result<(), failure::Error> { +const WARNING_SUPPRESSION: &str = "#![allow(clippy::all, warnings)]"; + +pub(crate) fn generate_code(params: CliCodegenParams) -> CliResult<()> { let CliCodegenParams { - additional_derives, + variables_derives, + response_derives, deprecation_strategy, no_formatting, output_directory, @@ -28,74 +40,122 @@ pub(crate) fn generate_code(params: CliCodegenParams) -> Result<(), failure::Err query_path, schema_path, selected_operation, + custom_scalars_module, + fragments_other_variant, + external_enums, + custom_variable_types, + custom_response_type, } = params; let deprecation_strategy = deprecation_strategy.as_ref().and_then(|s| s.parse().ok()); let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); - options.set_module_visibility( - syn::VisPublic { - pub_token: ::default(), - } - .into(), - ); + options.set_module_visibility(match _module_visibility { + Some(v) => match v.to_lowercase().as_str() { + "pub" => Visibility::Public(Pub::default()), + "inherited" => Visibility::Inherited, + _ => Visibility::Restricted(VisRestricted { + pub_token: Pub::default(), + in_token: None, + paren_token: Paren::default(), + path: syn::parse_str(&v).unwrap(), + }), + }, + None => Visibility::Public(Pub::default()), + }); + + options.set_fragments_other_variant(fragments_other_variant); if let Some(selected_operation) = selected_operation { options.set_operation_name(selected_operation); } - if let Some(additional_derives) = additional_derives { - options.set_additional_derives(additional_derives); + if let Some(variables_derives) = variables_derives { + options.set_variables_derives(variables_derives); + } + + if let Some(response_derives) = response_derives { + options.set_response_derives(response_derives); } if let Some(deprecation_strategy) = deprecation_strategy { options.set_deprecation_strategy(deprecation_strategy); } - let gen = generate_module_token_stream(query_path.clone(), &schema_path, options)?; + if let Some(external_enums) = external_enums { + options.set_extern_enums(external_enums); + } + + if let Some(custom_scalars_module) = custom_scalars_module { + let custom_scalars_module = syn::parse_str(&custom_scalars_module) + .map_err(|_| Error::message("Invalid custom scalar module path".to_owned()))?; + + options.set_custom_scalars_module(custom_scalars_module); + } + + if let Some(custom_variable_types) = custom_variable_types { + options.set_custom_variable_types(custom_variable_types.split(",").map(String::from).collect()); + } + + if let Some(custom_response_type) = custom_response_type { + options.set_custom_response_type(custom_response_type); + } + + let gen = generate_module_token_stream(query_path.clone(), &schema_path, options) + .map_err(|err| Error::message(format!("Error generating module code: {}", err)))?; - let generated_code = gen.to_string(); - let generated_code = if cfg!(feature = "rustfmt") && !no_formatting { - format(&generated_code) + let generated_code = format!("{}\n{}", WARNING_SUPPRESSION, gen); + let generated_code = if !no_formatting { + format(&generated_code)? } else { generated_code }; - let query_file_name: ::std::ffi::OsString = query_path - .file_name() - .map(ToOwned::to_owned) - .ok_or_else(|| format_err!("Failed to find a file name in the provided query path."))?; + let query_file_name: OsString = + query_path + .file_name() + .map(ToOwned::to_owned) + .ok_or_else(|| { + Error::message("Failed to find a file name in the provided query path.".to_owned()) + })?; let dest_file_path: PathBuf = output_directory .map(|output_dir| output_dir.join(query_file_name).with_extension("rs")) .unwrap_or_else(move || query_path.with_extension("rs")); - let mut file = File::create(dest_file_path)?; + log::info!("Writing generated query to {:?}", dest_file_path); + + let mut file = File::create(&dest_file_path).map_err(|err| { + Error::source_with_message( + err, + format!("Creating file at {}", dest_file_path.display()), + ) + })?; write!(file, "{}", generated_code)?; Ok(()) } -#[allow(unused_variables)] -fn format(codes: &str) -> String { - #[cfg(feature = "rustfmt")] - { - use rustfmt::{Config, Input, Session}; +fn format(code: &str) -> CliResult { + let binary = "rustfmt"; - let mut config = Config::default(); + let mut child = std::process::Command::new(binary) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .map_err(|err| Error::source_with_message(err, "Error spawning rustfmt".to_owned()))?; + let child_stdin = child.stdin.as_mut().unwrap(); + write!(child_stdin, "{}", code)?; - config.set().emit_mode(rustfmt_nightly::EmitMode::Stdout); - config.set().verbose(rustfmt_nightly::Verbosity::Quiet); + let output = child.wait_with_output()?; - let mut out = Vec::with_capacity(codes.len() * 2); - - Session::new(config, Some(&mut out)) - .format(Input::Text(codes.to_string())) - .unwrap_or_else(|err| panic!("rustfmt error: {}", err)); - - return String::from_utf8(out).unwrap(); + if !output.status.success() { + panic!( + "rustfmt error\n\n{}", + String::from_utf8_lossy(&output.stderr) + ); } - #[cfg(not(feature = "rustfmt"))] - unreachable!() + + Ok(String::from_utf8(output.stdout)?) } diff --git a/graphql_client_cli/src/graphql/introspection_query_with_isOneOf_specifiedByUrl.graphql b/graphql_client_cli/src/graphql/introspection_query_with_isOneOf_specifiedByUrl.graphql new file mode 100644 index 000000000..e78d209f9 --- /dev/null +++ b/graphql_client_cli/src/graphql/introspection_query_with_isOneOf_specifiedByUrl.graphql @@ -0,0 +1,101 @@ +query IntrospectionQueryWithIsOneOfSpecifiedByURL { + __schema { + queryType { + name + } + mutationType { + name + } + subscriptionType { + name + } + types { + ...FullTypeWithisOneOfSpecifiedByURL + } + directives { + name + description + locations + args { + ...InputValue + } + } + } +} + +fragment FullTypeWithisOneOfSpecifiedByURL on __Type { + kind + name + description + isOneOf + specifiedByURL + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } +} + +fragment InputValue on __InputValue { + name + description + type { + ...TypeRef + } + defaultValue +} + +fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } +} diff --git a/graphql_client_cli/src/graphql/introspection_query_with_is_one_of.graphql b/graphql_client_cli/src/graphql/introspection_query_with_is_one_of.graphql new file mode 100644 index 000000000..20faed646 --- /dev/null +++ b/graphql_client_cli/src/graphql/introspection_query_with_is_one_of.graphql @@ -0,0 +1,100 @@ +query IntrospectionQueryWithIsOneOf { + __schema { + queryType { + name + } + mutationType { + name + } + subscriptionType { + name + } + types { + ...FullTypeWithisOneOf + } + directives { + name + description + locations + args { + ...InputValue + } + } + } +} + +fragment FullTypeWithisOneOf on __Type { + kind + name + description + isOneOf + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } +} + +fragment InputValue on __InputValue { + name + description + type { + ...TypeRef + } + defaultValue +} + +fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } +} diff --git a/graphql_client_cli/src/graphql/introspection_query_with_specified_by.graphql b/graphql_client_cli/src/graphql/introspection_query_with_specified_by.graphql new file mode 100644 index 000000000..7c2db3b93 --- /dev/null +++ b/graphql_client_cli/src/graphql/introspection_query_with_specified_by.graphql @@ -0,0 +1,100 @@ +query IntrospectionQueryWithSpecifiedBy { + __schema { + queryType { + name + } + mutationType { + name + } + subscriptionType { + name + } + types { + ...FullTypeWithSpecifiedBy + } + directives { + name + description + locations + args { + ...InputValue + } + } + } +} + +fragment FullTypeWithSpecifiedBy on __Type { + kind + name + description + specifiedByURL + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } +} + +fragment InputValue on __InputValue { + name + description + type { + ...TypeRef + } + defaultValue +} + +fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } +} diff --git a/graphql_client_cli/src/graphql/introspection_schema.graphql b/graphql_client_cli/src/graphql/introspection_schema.graphql index 29f5587bf..c82bded4d 100644 --- a/graphql_client_cli/src/graphql/introspection_schema.graphql +++ b/graphql_client_cli/src/graphql/introspection_schema.graphql @@ -36,6 +36,14 @@ type __Type { # NON_NULL and LIST only ofType: __Type + + # may be non-null for custom SCALAR, otherwise null. + # https://spec.graphql.org/draft/#sec-Scalars.Custom-Scalars + specifiedByURL: String + specifiedBy: String + + # should be non-null for INPUT_OBJECT only + isOneOf: Boolean } type __Field { diff --git a/graphql_client_cli/src/introspection_queries.rs b/graphql_client_cli/src/introspection_queries.rs new file mode 100644 index 000000000..6b4b94dcf --- /dev/null +++ b/graphql_client_cli/src/introspection_queries.rs @@ -0,0 +1,41 @@ +use graphql_client::GraphQLQuery; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/graphql/introspection_schema.graphql", + query_path = "src/graphql/introspection_query.graphql", + response_derives = "Serialize", + variable_derives = "Deserialize" +)] +#[allow(dead_code)] +pub struct IntrospectionQuery; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/graphql/introspection_schema.graphql", + query_path = "src/graphql/introspection_query_with_is_one_of.graphql", + response_derives = "Serialize", + variable_derives = "Deserialize" +)] +#[allow(dead_code)] +pub struct IntrospectionQueryWithIsOneOf; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/graphql/introspection_schema.graphql", + query_path = "src/graphql/introspection_query_with_specified_by.graphql", + response_derives = "Serialize", + variable_derives = "Deserialize" +)] +#[allow(dead_code)] +pub struct IntrospectionQueryWithSpecifiedBy; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "src/graphql/introspection_schema.graphql", + query_path = "src/graphql/introspection_query_with_isOneOf_specifiedByUrl.graphql", + response_derives = "Serialize", + variable_derives = "Deserialize" +)] +#[allow(dead_code)] +pub struct IntrospectionQueryWithIsOneOfSpecifiedByURL; diff --git a/graphql_client_cli/src/introspect_schema.rs b/graphql_client_cli/src/introspection_schema.rs similarity index 62% rename from graphql_client_cli/src/introspect_schema.rs rename to graphql_client_cli/src/introspection_schema.rs index c9e5d52ac..9f3e348ff 100644 --- a/graphql_client_cli/src/introspect_schema.rs +++ b/graphql_client_cli/src/introspection_schema.rs @@ -1,38 +1,63 @@ -use failure::format_err; -use graphql_client::GraphQLQuery; +use crate::error::Error; +use crate::CliResult; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE}; use std::path::PathBuf; use std::str::FromStr; -#[derive(GraphQLQuery)] -#[graphql( - schema_path = "src/graphql/introspection_schema.graphql", - query_path = "src/graphql/introspection_query.graphql", - response_derives = "Serialize" -)] -#[allow(dead_code)] -struct IntrospectionQuery; +use crate::introspection_queries::{ + introspection_query, introspection_query_with_is_one_of, + introspection_query_with_is_one_of_specified_by_url, introspection_query_with_specified_by, +}; pub fn introspect_schema( location: &str, output: Option, authorization: Option, headers: Vec
, -) -> Result<(), failure::Error> { + no_ssl: bool, + is_one_of: bool, + specify_by_url: bool, +) -> CliResult<()> { use std::io::Write; let out: Box = match output { Some(path) => Box::new(::std::fs::File::create(path)?), - None => Box::new(::std::io::stdout()), + None => Box::new(std::io::stdout()), }; - let request_body: graphql_client::QueryBody<()> = graphql_client::QueryBody { + let mut request_body: graphql_client::QueryBody<()> = graphql_client::QueryBody { variables: (), query: introspection_query::QUERY, operation_name: introspection_query::OPERATION_NAME, }; - let client = reqwest::Client::new(); + if is_one_of { + request_body = graphql_client::QueryBody { + variables: (), + query: introspection_query_with_is_one_of::QUERY, + operation_name: introspection_query_with_is_one_of::OPERATION_NAME, + } + } + + if specify_by_url { + request_body = graphql_client::QueryBody { + variables: (), + query: introspection_query_with_specified_by::QUERY, + operation_name: introspection_query_with_specified_by::OPERATION_NAME, + } + } + + if is_one_of && specify_by_url { + request_body = graphql_client::QueryBody { + variables: (), + query: introspection_query_with_is_one_of_specified_by_url::QUERY, + operation_name: introspection_query_with_is_one_of_specified_by_url::OPERATION_NAME, + } + } + + let client = reqwest::blocking::Client::builder() + .danger_accept_invalid_certs(no_ssl) + .build()?; let mut req_builder = client.post(location).headers(construct_headers()); @@ -44,18 +69,27 @@ pub fn introspect_schema( req_builder = req_builder.bearer_auth(token.as_str()); }; - let mut res = req_builder.json(&request_body).send()?; + let res = req_builder.json(&request_body).send()?; if res.status().is_success() { // do nothing } else if res.status().is_server_error() { - println!("server error!"); + return Err(Error::message("server error!".into())); } else { - println!("Something else happened. Status: {:?}", res.status()); + let status = res.status(); + let error_message = match res.text() { + Ok(msg) => match serde_json::from_str::(&msg) { + Ok(json) => format!("HTTP {}\n{}", status, serde_json::to_string_pretty(&json)?), + Err(_) => format!("HTTP {}: {}", status, msg), + }, + Err(_) => format!("HTTP {}", status), + }; + return Err(Error::message(error_message)); } let json: serde_json::Value = res.json()?; serde_json::to_writer_pretty(out, &json)?; + Ok(()) } @@ -66,19 +100,19 @@ fn construct_headers() -> HeaderMap { headers } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct Header { name: String, value: String, } impl FromStr for Header { - type Err = failure::Error; + type Err = String; fn from_str(input: &str) -> Result { // error: colon required for name/value pair if !input.contains(':') { - return Err(format_err!( + return Err(format!( "Invalid header input. A colon is required to separate the name and value. [{}]", input )); @@ -91,7 +125,7 @@ impl FromStr for Header { // error: field name must be if name.is_empty() { - return Err(format_err!( + return Err(format!( "Invalid header input. Field name is required before colon. [{}]", input )); @@ -99,7 +133,7 @@ impl FromStr for Header { // error: no whitespace in field name if name.split_whitespace().count() > 1 { - return Err(format_err!( + return Err(format!( "Invalid header input. Whitespace not allowed in field name. [{}]", input )); @@ -120,12 +154,14 @@ mod tests { fn it_errors_invalid_headers() { // https://tools.ietf.org/html/rfc7230#section-3.2 - for input in vec![ + for input in [ "X-Name Value", // error: colon required for name/value pair ": Value", // error: field name must be "X Name: Value", // error: no whitespace in field name "X\tName: Value", // error: no whitespace in field name (tab) - ] { + ] + .iter() + { let header = Header::from_str(input); assert!(header.is_err(), "Expected error: [{}]", input); @@ -145,7 +181,7 @@ mod tests { value: "Value:".to_string(), }; - for (input, expected) in vec![ + for (input, expected) in [ ("X-Name: Value", &expected1), // ideal ("X-Name:Value", &expected1), // no optional whitespace ("X-Name: Value ", &expected1), // with optional whitespace @@ -154,11 +190,18 @@ mod tests { // not allowed per RFC, but we'll forgive ("X-Name : Value", &expected1), (" X-Name: Value", &expected1), - ] { + ] + .iter() + { let header = Header::from_str(input); assert!(header.is_ok(), "Expected ok: [{}]", input); - assert_eq!(header.unwrap(), *expected, "Expected equality: [{}]", input); + assert_eq!( + header.unwrap(), + **expected, + "Expected equality: [{}]", + input + ); } } } diff --git a/graphql_client_cli/src/main.rs b/graphql_client_cli/src/main.rs index f6dba1f8e..6721953bf 100644 --- a/graphql_client_cli/src/main.rs +++ b/graphql_client_cli/src/main.rs @@ -1,82 +1,135 @@ +mod error; +mod generate; +mod introspection_queries; +mod introspection_schema; + +use clap::Parser; use env_logger::fmt::{Color, Style, StyledValue}; use log::Level; -#[cfg(feature = "rustfmt")] -extern crate rustfmt_nightly as rustfmt; +use error::Error; -mod generate; -mod introspect_schema; use std::path::PathBuf; -use structopt::StructOpt; +use Cli::Generate; -#[derive(StructOpt)] +type CliResult = Result; + +#[derive(Parser)] +#[clap(author, about, version)] enum Cli { /// Get the schema from a live GraphQL API. The schema is printed to stdout. - #[structopt(name = "introspect-schema")] + #[clap(name = "introspect-schema")] IntrospectSchema { /// The URL of a GraphQL endpoint to introspect. schema_location: String, /// Where to write the JSON for the introspected schema. - #[structopt(parse(from_os_str))] - #[structopt(long = "output")] + #[arg(long = "output")] output: Option, - /// Set the contents of the Authorizaiton header. - #[structopt(long = "authorization")] + /// Set the contents of the Authorization header. + #[arg(long = "authorization")] authorization: Option, /// Specify custom headers. /// --header 'X-Name: Value' - #[structopt(long = "header")] - headers: Vec, + #[arg(long = "header")] + headers: Vec, + /// Disable ssl verification. + /// Default value is false. + #[clap(long = "no-ssl")] + no_ssl: bool, + /// Introspection Option: is-one-of will enable the @oneOf directive in the introspection query. + /// This is an proposed feature and is not compatible with many GraphQL servers. + /// Default value is false. + #[clap(long = "is-one-of")] + is_one_of: bool, + /// Introspection Option: specify-by-url will enable the @specifiedByURL directive in the introspection query. + /// This is an proposed feature and is not compatible with many GraphQL servers. + /// Default value is false. + #[clap(long = "specify-by-url")] + specify_by_url: bool, }, - #[structopt(name = "generate")] + #[clap(name = "generate")] Generate { /// Path to GraphQL schema file (.json or .graphql). - #[structopt(short = "s", long = "schema-path")] + #[clap(short = 's', long = "schema-path")] schema_path: PathBuf, /// Path to the GraphQL query file. query_path: PathBuf, /// Name of target query. If you don't set this parameter, cli generate all queries in query file. - #[structopt(short = "o", long = "selected-operation")] + #[clap(long = "selected-operation")] selected_operation: Option, - /// Additional derives that will be added to the generated structs and enums for the response and the variables. - /// --additional-derives='Serialize,PartialEq' - #[structopt(short = "a", long = "additional-derives")] - additional_derives: Option, + /// Additional derives that will be added to the generated structs and enums for the variables. + /// --variables-derives='Serialize,PartialEq' + #[clap(short = 'I', long = "variables-derives")] + variables_derives: Option, + /// Additional derives that will be added to the generated structs and enums for the response. + /// --response-derives='Serialize,PartialEq' + #[clap(short = 'O', long = "response-derives")] + response_derives: Option, /// You can choose deprecation strategy from allow, deny, or warn. /// Default value is warn. - #[structopt(short = "d", long = "deprecation-strategy")] + #[clap(short = 'd', long = "deprecation-strategy")] deprecation_strategy: Option, /// If you don't want to execute rustfmt to generated code, set this option. /// Default value is false. - /// Formating feature is disabled as default installation. - #[structopt(long = "no-formatting")] + #[clap(long = "no-formatting")] no_formatting: bool, /// You can choose module and target struct visibility from pub and private. /// Default value is pub. - #[structopt(short = "m", long = "module-visibility")] + #[clap(short = 'm', long = "module-visibility")] module_visibility: Option, /// The directory in which the code will be generated. /// /// If this option is omitted, the code will be generated next to the .graphql /// file, with the same name and the .rs extension. - #[structopt(short = "out", long = "output-directory")] + #[clap(short = 'o', long = "output-directory")] output_directory: Option, + /// The module where the custom scalar definitions are located. + /// --custom-scalars-module='crate::gql::custom_scalars' + #[clap(short = 'p', long = "custom-scalars-module")] + custom_scalars_module: Option, + /// A flag indicating if the enum representing the variants of a fragment union/interface should have a "other" variant + /// --fragments-other-variant + #[clap(long = "fragments-other-variant")] + fragments_other_variant: bool, + /// List of externally defined enum types. Type names must match those used in the schema exactly + #[clap(long = "external-enums", num_args(0..), action(clap::ArgAction::Append))] + external_enums: Option>, + /// Custom variable types to use + /// --custom-variable-types='external_crate::MyStruct,external_crate::MyStruct2' + #[clap(long = "custom-variable_types")] + custom_variable_types: Option, + /// Custom response type to use + /// --custom-response-type='external_crate::MyResponse' + #[clap(long = "custom-response-type")] + custom_response_type: Option, }, } -fn main() -> Result<(), failure::Error> { +fn main() -> CliResult<()> { set_env_logger(); - let cli = Cli::from_args(); + let cli = Cli::parse(); match cli { Cli::IntrospectSchema { schema_location, output, authorization, headers, - } => introspect_schema::introspect_schema(&schema_location, output, authorization, headers), - Cli::Generate { - additional_derives, + no_ssl, + is_one_of, + specify_by_url, + } => introspection_schema::introspect_schema( + &schema_location, + output, + authorization, + headers, + no_ssl, + is_one_of, + specify_by_url, + ), + Generate { + variables_derives, + response_derives, deprecation_strategy, module_visibility, no_formatting, @@ -84,15 +137,26 @@ fn main() -> Result<(), failure::Error> { query_path, schema_path, selected_operation, + custom_scalars_module, + fragments_other_variant, + external_enums, + custom_variable_types, + custom_response_type, } => generate::generate_code(generate::CliCodegenParams { - additional_derives, - deprecation_strategy, - module_visibility, - no_formatting, - output_directory, query_path, schema_path, selected_operation, + variables_derives, + response_derives, + deprecation_strategy, + no_formatting, + module_visibility, + output_directory, + custom_scalars_module, + fragments_other_variant, + external_enums, + custom_variable_types, + custom_response_type, }), } } @@ -122,7 +186,7 @@ fn set_env_logger() { .init(); } -fn colored_level<'a>(style: &'a mut Style, level: Level) -> StyledValue<'a, &'static str> { +fn colored_level(style: &mut Style, level: Level) -> StyledValue<'_, &'static str> { match level { Level::Trace => style.set_color(Color::Magenta).value("TRACE"), Level::Debug => style.set_color(Color::Blue).value("DEBUG"), diff --git a/graphql_client_codegen/.gitignore b/graphql_client_codegen/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/graphql_client_codegen/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/graphql_client_codegen/Cargo.toml b/graphql_client_codegen/Cargo.toml index 044be17d7..8f30cbb47 100644 --- a/graphql_client_codegen/Cargo.toml +++ b/graphql_client_codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graphql_client_codegen" -version = "0.8.0" +version = "0.14.0" authors = ["Tom Houlé "] description = "Utility crate for graphql_client" license = "Apache-2.0 OR MIT" @@ -8,13 +8,12 @@ repository = "https://github.com/graphql-rust/graphql-client" edition = "2018" [dependencies] -failure = "0.1" -graphql-introspection-query = { path = "../graphql-introspection-query" } -graphql-parser = "^0.2" -heck = "0.3" +graphql-introspection-query = { version = "0.2.0", path = "../graphql-introspection-query" } +graphql-parser = "0.4" +heck = ">=0.4, <=0.5" lazy_static = "1.3" proc-macro2 = { version = "^1.0", features = [] } quote = "^1.0" serde_json = "1.0" serde = { version = "^1.0", features = ["derive"] } -syn = "^1.0" +syn = { version = "^2.0", features = [ "full" ] } diff --git a/graphql_client_codegen/src/codegen.rs b/graphql_client_codegen/src/codegen.rs index fa8ad3d1e..e33bb1b11 100644 --- a/graphql_client_codegen/src/codegen.rs +++ b/graphql_client_codegen/src/codegen.rs @@ -1,154 +1,52 @@ -use crate::fragments::GqlFragment; -use crate::operations::Operation; -use crate::query::QueryContext; -use crate::schema; -use crate::selection::Selection; -use failure::*; -use graphql_parser::query; -use proc_macro2::TokenStream; -use quote::*; - -/// Selects the first operation matching `struct_name`. Returns `None` when the query document defines no operation, or when the selected operation does not match any defined operation. -pub(crate) fn select_operation<'query>( - query: &'query query::Document, - struct_name: &str, -) -> Option> { - let operations = all_operations(query); - - operations - .iter() - .find(|op| op.name == struct_name) - .map(ToOwned::to_owned) -} +mod enums; +mod inputs; +mod selection; +mod shared; -pub(crate) fn all_operations(query: &query::Document) -> Vec> { - let mut operations: Vec> = Vec::new(); - - for definition in &query.definitions { - if let query::Definition::Operation(op) = definition { - operations.push(op.into()); - } - } - operations -} +use crate::{ + query::*, + schema::{InputId, TypeId}, + type_qualifiers::GraphqlTypeQualifier, + GeneralError, GraphQLClientCodegenOptions, +}; +use heck::ToSnakeCase; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{quote, ToTokens}; +use selection::*; +use std::collections::BTreeMap; /// The main code generation function. pub(crate) fn response_for_query( - schema: &schema::Schema<'_>, - query: &query::Document, - operation: &Operation<'_>, - options: &crate::GraphQLClientCodegenOptions, -) -> Result { - let mut context = QueryContext::new(schema, options.deprecation_strategy()); - - if let Some(derives) = options.additional_derives() { - context.ingest_additional_derives(&derives)?; - } + operation_id: OperationId, + options: &GraphQLClientCodegenOptions, + query: BoundQuery<'_>, +) -> Result { + let serde = options.serde_path(); - let mut definitions = Vec::new(); - - for definition in &query.definitions { - match definition { - query::Definition::Operation(_op) => (), - query::Definition::Fragment(fragment) => { - let &query::TypeCondition::On(ref on) = &fragment.type_condition; - let on = schema.fragment_target(on).ok_or_else(|| { - format_err!( - "Fragment {} is defined on unknown type: {}", - &fragment.name, - on, - ) - })?; - context.fragments.insert( - &fragment.name, - GqlFragment { - name: &fragment.name, - selection: Selection::from(&fragment.selection_set), - on, - is_required: false.into(), - }, - ); - } - } - } + let all_used_types = all_used_types(operation_id, &query); + let response_derives = render_derives(options.all_response_derives()); + let variable_derives = render_derives(options.all_variable_derives()); - let response_data_fields = { - let root_name = operation.root_name(&context.schema); - let opt_definition = context.schema.objects.get(&root_name); - let definition = if let Some(definition) = opt_definition { - definition - } else { - panic!( - "operation type '{:?}' not in schema", - operation.operation_type - ); - }; - let prefix = &operation.name; - let selection = &operation.selection; - - if operation.is_subscription() && selection.len() > 1 { - Err(format_err!( - "{}", - crate::constants::MULTIPLE_SUBSCRIPTION_FIELDS_ERROR - ))? - } + let scalar_definitions = generate_scalar_definitions(&all_used_types, options, query); + let enum_definitions = enums::generate_enum_definitions(&all_used_types, options, query); + let fragment_definitions = + generate_fragment_definitions(&all_used_types, &response_derives, options, &query); + let input_object_definitions = inputs::generate_input_object_definitions( + &all_used_types, + options, + &variable_derives, + &query, + ); - definitions.extend(definition.field_impls_for_selection(&context, &selection, &prefix)?); - definition.response_fields_for_selection(&context, &selection, &prefix)? - }; + let variables_struct = + generate_variables_struct(operation_id, &variable_derives, options, &query); - let enum_definitions = context.schema.enums.values().filter_map(|enm| { - if enm.is_required.get() { - Some(enm.to_rust(&context)) - } else { - None - } - }); - let fragment_definitions: Result, _> = context - .fragments - .values() - .filter_map(|fragment| { - if fragment.is_required.get() { - Some(fragment.to_rust(&context)) - } else { - None - } - }) - .collect(); - let fragment_definitions = fragment_definitions?; - let variables_struct = operation.expand_variables(&context); - - let input_object_definitions: Result, _> = context - .schema - .inputs - .values() - .filter_map(|i| { - if i.is_required.get() { - Some(i.to_rust(&context)) - } else { - None - } - }) - .collect(); - let input_object_definitions = input_object_definitions?; - - let scalar_definitions: Vec = context - .schema - .scalars - .values() - .filter_map(|s| { - if s.is_required.get() { - Some(s.to_rust()) - } else { - None - } - }) - .collect(); - - let response_derives = context.response_derives(); + let definitions = + render_response_data_fields(operation_id, options, &query)?.render(&response_derives); - Ok(quote! { - use serde::{Serialize, Deserialize}; + let q = quote! { + use #serde::{Serialize, Deserialize}; + use super::*; #[allow(dead_code)] type Boolean = bool; @@ -161,21 +59,289 @@ pub(crate) fn response_for_query( #(#scalar_definitions)* + #(#enum_definitions)* + #(#input_object_definitions)* - #(#enum_definitions)* + #variables_struct #(#fragment_definitions)* - #(#definitions)* + #definitions + }; - #variables_struct + Ok(q) +} + +fn generate_variables_struct( + operation_id: OperationId, + variable_derives: &impl quote::ToTokens, + options: &GraphQLClientCodegenOptions, + query: &BoundQuery<'_>, +) -> TokenStream { + let serde = options.serde_path(); + let serde_path = serde.to_token_stream().to_string(); + + if operation_has_no_variables(operation_id, query.query) { + return quote!( + #variable_derives + #[serde(crate = #serde_path)] + pub struct Variables; + ); + } + + let variable_fields = walk_operation_variables(operation_id, query.query) + .map(|(_id, variable)| generate_variable_struct_field(variable, options, query)); + let variable_defaults = + walk_operation_variables(operation_id, query.query).map(|(_id, variable)| { + let method_name = format!("default_{}", variable.name); + let method_name = Ident::new(&method_name, Span::call_site()); + let method_return_type = render_variable_field_type(variable, options, query); + + variable.default.as_ref().map(|default| { + let value = graphql_parser_value_to_literal( + default, + variable.r#type.id, + variable + .r#type + .qualifiers + .first() + .map(|qual| !qual.is_required()) + .unwrap_or(true), + query, + ); + + quote!( + pub fn #method_name() -> #method_return_type { + #value + } + ) + }) + }); + + let variables_struct = quote!( + #variable_derives + #[serde(crate = #serde_path)] + pub struct Variables { + #(#variable_fields,)* + } + + impl Variables { + #(#variable_defaults)* + } + ); + + variables_struct +} + +fn generate_variable_struct_field( + variable: &ResolvedVariable, + options: &GraphQLClientCodegenOptions, + query: &BoundQuery<'_>, +) -> TokenStream { + let snake_case_name = variable.name.to_snake_case(); + let safe_name = shared::keyword_replace(&snake_case_name); + let ident = Ident::new(&safe_name, Span::call_site()); + let rename_annotation = shared::field_rename_annotation(&variable.name, &safe_name); + let skip_serializing_annotation = if *options.skip_serializing_none() { + if variable.r#type.qualifiers.first() != Some(&GraphqlTypeQualifier::Required) { + Some(quote!(#[serde(skip_serializing_if = "Option::is_none")])) + } else { + None + } + } else { + None + }; + let r#type = render_variable_field_type(variable, options, query); + + quote::quote!(#skip_serializing_annotation #rename_annotation pub #ident : #r#type) +} + +fn generate_scalar_definitions<'a, 'schema: 'a>( + all_used_types: &'a crate::query::UsedTypes, + options: &'a GraphQLClientCodegenOptions, + query: BoundQuery<'schema>, +) -> impl Iterator + 'a { + all_used_types + .scalars(query.schema) + .map(move |(_id, scalar)| { + let ident = syn::Ident::new( + options.normalization().scalar_name(&scalar.name).as_ref(), + proc_macro2::Span::call_site(), + ); + + if let Some(custom_scalars_module) = options.custom_scalars_module() { + quote!(type #ident = #custom_scalars_module::#ident;) + } else { + quote!(type #ident = super::#ident;) + } + }) +} - #response_derives +fn render_derives<'a>(derives: impl Iterator) -> impl quote::ToTokens { + let idents = derives.map(|s| { + syn::parse_str::(s) + .map_err(|e| format!("couldn't parse {} as a derive Path: {}", s, e)) + .unwrap() + }); - pub struct ResponseData { - #(#response_data_fields,)* + quote!(#[derive(#(#idents),*)]) +} + +fn render_variable_field_type( + variable: &ResolvedVariable, + options: &GraphQLClientCodegenOptions, + query: &BoundQuery<'_>, +) -> TokenStream { + let normalized_name = options + .normalization() + .input_name(variable.type_name(query.schema)); + let safe_name = shared::keyword_replace(normalized_name.clone()); + let full_name = Ident::new(safe_name.as_ref(), Span::call_site()); + + decorate_type(&full_name, &variable.r#type.qualifiers) +} + +fn decorate_type(ident: &Ident, qualifiers: &[GraphqlTypeQualifier]) -> TokenStream { + let mut qualified = quote!(#ident); + + let mut non_null = false; + + // Note: we iterate over qualifiers in reverse because it is more intuitive. This + // means we start from the _inner_ type and make our way to the outside. + for qualifier in qualifiers.iter().rev() { + match (non_null, qualifier) { + // We are in non-null context, and we wrap the non-null type into a list. + // We switch back to null context. + (true, GraphqlTypeQualifier::List) => { + qualified = quote!(Vec<#qualified>); + non_null = false; + } + // We are in nullable context, and we wrap the nullable type into a list. + (false, GraphqlTypeQualifier::List) => { + qualified = quote!(Vec>); + } + // We are in non-nullable context, but we can't double require a type + // (!!). + (true, GraphqlTypeQualifier::Required) => panic!("double required annotation"), + // We are in nullable context, and we switch to non-nullable context. + (false, GraphqlTypeQualifier::Required) => { + non_null = true; + } } + } + + // If we are in nullable context at the end of the iteration, we wrap the whole + // type with an Option. + if !non_null { + qualified = quote!(Option<#qualified>); + } + + qualified +} + +fn generate_fragment_definitions<'a>( + all_used_types: &'a UsedTypes, + response_derives: &'a impl quote::ToTokens, + options: &'a GraphQLClientCodegenOptions, + query: &'a BoundQuery<'a>, +) -> impl Iterator + 'a { + all_used_types.fragment_ids().map(move |fragment_id| { + selection::render_fragment(fragment_id, options, query).render(&response_derives) + }) +} + +/// For default value constructors. +fn graphql_parser_value_to_literal<'doc, T>( + value: &graphql_parser::query::Value<'doc, T>, + ty: TypeId, + is_optional: bool, + query: &BoundQuery<'_>, +) -> TokenStream +where + T: graphql_parser::query::Text<'doc>, + T::Value: quote::ToTokens, +{ + use graphql_parser::query::Value; + + let inner = match value { + Value::Boolean(b) => { + if *b { + quote!(true) + } else { + quote!(false) + } + } + Value::String(s) => quote!(#s.to_string()), + Value::Variable(_) => panic!("variable in variable"), + Value::Null => panic!("null as default value"), + Value::Float(f) => quote!(#f), + Value::Int(i) => { + let i = i.as_i64(); + quote!(#i) + } + Value::Enum(en) => quote!(#en), + Value::List(inner) => { + let elements = inner + .iter() + .map(|val| graphql_parser_value_to_literal(val, ty, false, query)); + quote! { + vec![ + #(#elements,)* + ] + } + } + Value::Object(obj) => ty + .as_input_id() + .map(|input_id| render_object_literal(obj, input_id, query)) + .unwrap_or_else(|| { + quote!(compile_error!( + "Object literal on a non-input-object field." + )) + }), + }; + + if is_optional { + quote!(Some(#inner)) + } else { + inner + } +} + +/// For default value constructors. +fn render_object_literal<'doc, T>( + object_map: &BTreeMap>, + input_id: InputId, + query: &BoundQuery<'_>, +) -> TokenStream +where + T: graphql_parser::query::Text<'doc>, + T::Value: quote::ToTokens, +{ + let input = query.schema.get_input(input_id); + let constructor = Ident::new(&input.name, Span::call_site()); + let fields: Vec = input + .fields + .iter() + .map(|(name, r#type)| { + let field_name = Ident::new(name, Span::call_site()); + let provided_value = object_map.get(name); + match provided_value { + Some(default_value) => { + let value = graphql_parser_value_to_literal( + default_value, + r#type.id, + r#type.is_optional(), + query, + ); + quote!(#field_name: #value) + } + None => quote!(#field_name: None), + } + }) + .collect(); + quote!(#constructor { + #(#fields,)* }) } diff --git a/graphql_client_codegen/src/codegen/enums.rs b/graphql_client_codegen/src/codegen/enums.rs new file mode 100644 index 000000000..adc59a899 --- /dev/null +++ b/graphql_client_codegen/src/codegen/enums.rs @@ -0,0 +1,90 @@ +use crate::{ + codegen::render_derives, codegen_options::GraphQLClientCodegenOptions, query::BoundQuery, +}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote; + +/** + * About rust keyword escaping: variant_names and constructors must be escaped, + * variant_str not. + * Example schema: enum AnEnum { where \n self } + * Generated "variant_names" enum: pub enum AnEnum { where_, self_, Other(String), } + * Generated serialize line: "AnEnum::where_ => "where"," + */ +pub(super) fn generate_enum_definitions<'a, 'schema: 'a>( + all_used_types: &'a crate::query::UsedTypes, + options: &'a GraphQLClientCodegenOptions, + query: BoundQuery<'schema>, +) -> impl Iterator + 'a { + let serde = options.serde_path(); + let traits = options + .all_response_derives() + .chain(options.all_variable_derives()) + .filter(|d| !&["Serialize", "Deserialize", "Default"].contains(d)) + // Use BTreeSet instead of HashSet for a stable ordering. + .collect::>(); + let derives = render_derives(traits.into_iter()); + let normalization = options.normalization(); + + all_used_types.enums(query.schema) + .filter(move |(_id, r#enum)| !options.extern_enums().contains(&r#enum.name)) + .map(move |(_id, r#enum)| { + let variant_names: Vec = r#enum + .variants + .iter() + .map(|v| { + let safe_name = super::shared::keyword_replace(v.as_str()); + let name = normalization.enum_variant(safe_name.as_ref()); + let name = Ident::new(&name, Span::call_site()); + + quote!(#name) + }) + .collect(); + let variant_names = &variant_names; + let name_ident = normalization.enum_name(r#enum.name.as_str()); + let name_ident = Ident::new(&name_ident, Span::call_site()); + let constructors: Vec<_> = r#enum + .variants + .iter() + .map(|v| { + let safe_name = super::shared::keyword_replace(v); + let name = normalization.enum_variant(safe_name.as_ref()); + let v = Ident::new(&name, Span::call_site()); + + quote!(#name_ident::#v) + }) + .collect(); + let constructors = &constructors; + let variant_str: Vec<&str> = r#enum.variants.iter().map(|s| s.as_str()).collect(); + let variant_str = &variant_str; + + let name = name_ident; + + quote! { + #derives + pub enum #name { + #(#variant_names,)* + Other(String), + } + + impl #serde::Serialize for #name { + fn serialize(&self, ser: S) -> Result { + ser.serialize_str(match *self { + #(#constructors => #variant_str,)* + #name::Other(ref s) => &s, + }) + } + } + + impl<'de> #serde::Deserialize<'de> for #name { + fn deserialize>(deserializer: D) -> Result { + let s: String = #serde::Deserialize::deserialize(deserializer)?; + + match s.as_str() { + #(#variant_str => Ok(#constructors),)* + _ => Ok(#name::Other(s)), + } + } + } + }}) +} diff --git a/graphql_client_codegen/src/codegen/inputs.rs b/graphql_client_codegen/src/codegen/inputs.rs new file mode 100644 index 000000000..d8cc10808 --- /dev/null +++ b/graphql_client_codegen/src/codegen/inputs.rs @@ -0,0 +1,153 @@ +use super::shared::{field_rename_annotation, keyword_replace}; +use crate::{ + codegen_options::GraphQLClientCodegenOptions, + query::{BoundQuery, UsedTypes}, + schema::{input_is_recursive_without_indirection, StoredInputType}, + type_qualifiers::GraphqlTypeQualifier, +}; +use heck::{ToSnakeCase, ToUpperCamelCase}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{quote, ToTokens}; + +pub(super) fn generate_input_object_definitions( + all_used_types: &UsedTypes, + options: &GraphQLClientCodegenOptions, + variable_derives: &impl quote::ToTokens, + query: &BoundQuery<'_>, +) -> Vec { + let custom_variable_types = options.custom_variable_types(); + all_used_types + .inputs(query.schema) + .map(|(input_id, input)| { + let custom_variable_type = query.query.variables.iter() + .enumerate() + .find(|(_, v) | v.r#type.id.as_input_id().is_some_and(|i| i == input_id)) + .map(|(index, _)| custom_variable_types.get(index)) + .flatten(); + if let Some(custom_type) = custom_variable_type { + generate_type_def(input, options, custom_type) + } else if input.is_one_of { + generate_enum(input, options, variable_derives, query) + } else { + generate_struct(input, options, variable_derives, query) + } + }) + .collect() +} + +fn generate_type_def( + input: &StoredInputType, + options: &GraphQLClientCodegenOptions, + custom_type: &String, +) -> TokenStream { + let custom_type = syn::parse_str::(custom_type).unwrap(); + let normalized_name = options.normalization().input_name(input.name.as_str()); + let safe_name = keyword_replace(normalized_name); + let struct_name = Ident::new(safe_name.as_ref(), Span::call_site()); + quote!(pub type #struct_name = #custom_type;) +} + +fn generate_struct( + input: &StoredInputType, + options: &GraphQLClientCodegenOptions, + variable_derives: &impl quote::ToTokens, + query: &BoundQuery<'_>, +) -> TokenStream { + let serde = options.serde_path(); + let serde_path = serde.to_token_stream().to_string(); + + let normalized_name = options.normalization().input_name(input.name.as_str()); + let safe_name = keyword_replace(normalized_name); + let struct_name = Ident::new(safe_name.as_ref(), Span::call_site()); + + let fields = input.fields.iter().map(|(field_name, field_type)| { + let safe_field_name = keyword_replace(field_name.to_snake_case()); + let annotation = field_rename_annotation(field_name, safe_field_name.as_ref()); + let name_ident = Ident::new(safe_field_name.as_ref(), Span::call_site()); + let normalized_field_type_name = options + .normalization() + .field_type(field_type.id.name(query.schema)); + let optional_skip_serializing_none = + if *options.skip_serializing_none() && field_type.is_optional() { + Some(quote!(#[serde(skip_serializing_if = "Option::is_none")])) + } else { + None + }; + let type_name = Ident::new(normalized_field_type_name.as_ref(), Span::call_site()); + let field_type_tokens = super::decorate_type(&type_name, &field_type.qualifiers); + let field_type = if field_type + .id + .as_input_id() + .map(|input_id| input_is_recursive_without_indirection(input_id, query.schema)) + .unwrap_or(false) + { + quote!(Box<#field_type_tokens>) + } else { + field_type_tokens + }; + + quote!( + #optional_skip_serializing_none + #annotation pub #name_ident: #field_type + ) + }); + + quote! { + #variable_derives + #[serde(crate = #serde_path)] + pub struct #struct_name{ + #(#fields,)* + } + } +} + +fn generate_enum( + input: &StoredInputType, + options: &GraphQLClientCodegenOptions, + variable_derives: &impl quote::ToTokens, + query: &BoundQuery<'_>, +) -> TokenStream { + let normalized_name = options.normalization().input_name(input.name.as_str()); + let safe_name = keyword_replace(normalized_name); + let enum_name = Ident::new(safe_name.as_ref(), Span::call_site()); + + let variants = input.fields.iter().map(|(field_name, field_type)| { + let variant_name = field_name.to_upper_camel_case(); + let safe_variant_name = keyword_replace(&variant_name); + + let annotation = field_rename_annotation(field_name.as_ref(), &variant_name); + let name_ident = Ident::new(safe_variant_name.as_ref(), Span::call_site()); + + let normalized_field_type_name = options + .normalization() + .field_type(field_type.id.name(query.schema)); + let type_name = Ident::new(normalized_field_type_name.as_ref(), Span::call_site()); + + // Add the required qualifier so that the variant's field isn't wrapped in Option + let mut qualifiers = vec![GraphqlTypeQualifier::Required]; + qualifiers.extend(field_type.qualifiers.iter().cloned()); + + let field_type_tokens = super::decorate_type(&type_name, &qualifiers); + let field_type = if field_type + .id + .as_input_id() + .map(|input_id| input_is_recursive_without_indirection(input_id, query.schema)) + .unwrap_or(false) + { + quote!(Box<#field_type_tokens>) + } else { + field_type_tokens + }; + + quote!( + #annotation #name_ident(#field_type) + ) + }); + + quote! { + #variable_derives + pub enum #enum_name{ + #(#variants,)* + } + } +} diff --git a/graphql_client_codegen/src/codegen/selection.rs b/graphql_client_codegen/src/codegen/selection.rs new file mode 100644 index 000000000..ec1703b82 --- /dev/null +++ b/graphql_client_codegen/src/codegen/selection.rs @@ -0,0 +1,680 @@ +//! Code generation for the selection on an operation or a fragment. + +use crate::{ + codegen::{ + decorate_type, + shared::{field_rename_annotation, keyword_replace}, + }, + deprecation::DeprecationStrategy, + query::{ + fragment_is_recursive, full_path_prefix, BoundQuery, InlineFragment, OperationId, + ResolvedFragment, ResolvedFragmentId, SelectedField, Selection, SelectionId, + }, + schema::{Schema, TypeId}, + type_qualifiers::GraphqlTypeQualifier, + GraphQLClientCodegenOptions, + GeneralError, +}; +use heck::*; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{quote, ToTokens}; +use std::borrow::Cow; +use syn::Path; + +pub(crate) fn render_response_data_fields<'a>( + operation_id: OperationId, + options: &'a GraphQLClientCodegenOptions, + query: &'a BoundQuery<'a>, +) -> Result, GeneralError> { + let operation = query.query.get_operation(operation_id); + let mut expanded_selection = ExpandedSelection { + query, + types: Vec::with_capacity(8), + aliases: Vec::new(), + variants: Vec::new(), + fields: Vec::with_capacity(operation.selection_set.len()), + options, + }; + + let response_data_type_id = expanded_selection.push_type(ExpandedType { + name: Cow::Borrowed("ResponseData"), + }); + + if let Some(custom_response_type) = options.custom_response_type() { + if operation.selection_set.len() == 1 { + let selection_id = operation.selection_set[0]; + let selection_field = query.query.get_selection(selection_id).as_selected_field() + .ok_or_else(|| GeneralError(format!("Custom response type {custom_response_type} will only work on fields")))?; + calculate_custom_response_type_selection(&mut expanded_selection, response_data_type_id, custom_response_type, selection_id, selection_field); + return Ok(expanded_selection); + } else { + return Err(GeneralError(format!("Custom response type {custom_response_type} requires single selection field"))); + } + } + + calculate_selection( + &mut expanded_selection, + &operation.selection_set, + response_data_type_id, + TypeId::Object(operation.object_id), + options, + ); + + Ok(expanded_selection) +} + +fn calculate_custom_response_type_selection<'a>( + context: &mut ExpandedSelection<'a>, + struct_id: ResponseTypeId, + custom_response_type: &'a String, + selection_id: SelectionId, + field: &'a SelectedField) +{ + let (graphql_name, rust_name) = context.field_name(field); + let struct_name_string = full_path_prefix(selection_id, context.query); + let field = context.query.schema.get_field(field.field_id); + context.push_field(ExpandedField { + struct_id, + graphql_name: Some(graphql_name), + rust_name, + field_type_qualifiers: &field.r#type.qualifiers, + field_type: struct_name_string.clone().into(), + flatten: false, + boxed: false, + deprecation: field.deprecation(), + }); + + let struct_id = context.push_type(ExpandedType { + name: struct_name_string.into(), + }); + context.push_type_alias(TypeAlias { + name: custom_response_type.as_str(), + struct_id, + boxed: false, + }); +} + +pub(super) fn render_fragment<'a>( + fragment_id: ResolvedFragmentId, + options: &'a GraphQLClientCodegenOptions, + query: &'a BoundQuery<'a>, +) -> ExpandedSelection<'a> { + let fragment = query.query.get_fragment(fragment_id); + let mut expanded_selection = ExpandedSelection { + query, + aliases: Vec::new(), + types: Vec::with_capacity(8), + variants: Vec::new(), + fields: Vec::with_capacity(fragment.selection_set.len()), + options, + }; + + let response_type_id = expanded_selection.push_type(ExpandedType { + name: fragment.name.as_str().into(), + }); + + calculate_selection( + &mut expanded_selection, + &fragment.selection_set, + response_type_id, + fragment.on, + options, + ); + + expanded_selection +} + +/// A sub-selection set (spread) on one of the variants of a union or interface. +enum VariantSelection<'a> { + InlineFragment(&'a InlineFragment), + FragmentSpread((ResolvedFragmentId, &'a ResolvedFragment)), +} + +impl<'a> VariantSelection<'a> { + /// The second argument is the parent type id, so it can be excluded. + fn from_selection( + selection: &'a Selection, + type_id: TypeId, + query: &BoundQuery<'a>, + ) -> Option> { + match selection { + Selection::InlineFragment(inline_fragment) => { + Some(VariantSelection::InlineFragment(inline_fragment)) + } + Selection::FragmentSpread(fragment_id) => { + let fragment = query.query.get_fragment(*fragment_id); + + if fragment.on == type_id { + // The selection is on the type itself. + None + } else { + // The selection is on one of the variants of the type. + Some(VariantSelection::FragmentSpread((*fragment_id, fragment))) + } + } + Selection::Field(_) | Selection::Typename => None, + } + } + + fn variant_type_id(&self) -> TypeId { + match self { + VariantSelection::InlineFragment(f) => f.type_id, + VariantSelection::FragmentSpread((_id, f)) => f.on, + } + } +} + +fn calculate_selection<'a>( + context: &mut ExpandedSelection<'a>, + selection_set: &[SelectionId], + struct_id: ResponseTypeId, + type_id: TypeId, + options: &'a GraphQLClientCodegenOptions, +) { + // If the selection only contains a fragment, replace the selection with + // that fragment. + if selection_set.len() == 1 { + if let Selection::FragmentSpread(fragment_id) = + context.query.query.get_selection(selection_set[0]) + { + let fragment = context.query.query.get_fragment(*fragment_id); + context.push_type_alias(TypeAlias { + name: &fragment.name, + struct_id, + boxed: fragment_is_recursive(*fragment_id, context.query.query), + }); + return; + } + } + + // If we are on a union or an interface, we need to generate an enum that matches the variants _exhaustively_. + { + let variants: Option> = match type_id { + TypeId::Interface(interface_id) => { + let variants = context + .query + .schema + .objects() + .filter(|(_, obj)| obj.implements_interfaces.contains(&interface_id)) + .map(|(id, _)| TypeId::Object(id)); + + Some(variants.collect::>().into()) + } + TypeId::Union(union_id) => { + let union = context.schema().get_union(union_id); + Some(union.variants.as_slice().into()) + } + _ => None, + }; + + if let Some(variants) = variants { + let variant_selections: Vec<(SelectionId, &Selection, VariantSelection<'_>)> = + selection_set + .iter() + .map(|id| (id, context.query.query.get_selection(*id))) + .filter_map(|(id, selection)| { + VariantSelection::from_selection(selection, type_id, context.query) + .map(|variant_selection| (*id, selection, variant_selection)) + }) + .collect(); + + // For each variant, get the corresponding fragment spreads and + // inline fragments, or default to an empty variant (one with no + // associated data). + for variant_type_id in variants.as_ref() { + let variant_name_str = variant_type_id.name(context.schema()); + + let variant_selections: Vec<_> = variant_selections + .iter() + .filter(|(_id, _selection_ref, variant)| { + variant.variant_type_id() == *variant_type_id + }) + .collect(); + + if let Some((selection_id, selection, _variant)) = variant_selections.first() { + let mut variant_struct_name_str = + full_path_prefix(*selection_id, context.query); + variant_struct_name_str.reserve(2 + variant_name_str.len()); + variant_struct_name_str.push_str("On"); + variant_struct_name_str.push_str(variant_name_str); + + context.push_variant(ExpandedVariant { + name: variant_name_str.into(), + variant_type: Some(variant_struct_name_str.clone().into()), + on: struct_id, + is_default_variant: false, + }); + + let expanded_type = ExpandedType { + name: variant_struct_name_str.into(), + }; + + let struct_id = context.push_type(expanded_type); + + if variant_selections.len() == 1 { + if let VariantSelection::FragmentSpread((fragment_id, fragment)) = + variant_selections[0].2 + { + context.push_type_alias(TypeAlias { + boxed: fragment_is_recursive(fragment_id, context.query.query), + name: &fragment.name, + struct_id, + }); + continue; + } + } + + for (_selection_id, _selection, variant_selection) in variant_selections { + match variant_selection { + VariantSelection::InlineFragment(_) => { + calculate_selection( + context, + selection.subselection(), + struct_id, + *variant_type_id, + options, + ); + } + VariantSelection::FragmentSpread((fragment_id, fragment)) => context + .push_field(ExpandedField { + field_type: fragment.name.as_str().into(), + field_type_qualifiers: &[GraphqlTypeQualifier::Required], + flatten: true, + graphql_name: None, + rust_name: fragment.name.to_snake_case().into(), + struct_id, + deprecation: None, + boxed: fragment_is_recursive(*fragment_id, context.query.query), + }), + } + } + } else { + context.push_variant(ExpandedVariant { + name: variant_name_str.into(), + on: struct_id, + variant_type: None, + is_default_variant: false, + }); + } + } + + if *options.fragments_other_variant() { + context.push_variant(ExpandedVariant { + name: "Unknown".into(), + on: struct_id, + variant_type: None, + is_default_variant: true, + }); + } + } + } + + for id in selection_set { + let selection = context.query.query.get_selection(*id); + + match selection { + Selection::Field(field) => { + let (graphql_name, rust_name) = context.field_name(field); + let schema_field = field.schema_field(context.schema()); + let field_type_id = schema_field.r#type.id; + + match field_type_id { + TypeId::Enum(enm) => { + context.push_field(ExpandedField { + graphql_name: Some(graphql_name), + rust_name, + struct_id, + field_type: options + .normalization() + .field_type(&context.schema().get_enum(enm).name), + field_type_qualifiers: &schema_field.r#type.qualifiers, + flatten: false, + deprecation: schema_field.deprecation(), + boxed: false, + }); + } + TypeId::Scalar(scalar) => { + context.push_field(ExpandedField { + field_type: options + .normalization() + .field_type(context.schema().get_scalar(scalar).name.as_str()), + field_type_qualifiers: &field + .schema_field(context.schema()) + .r#type + .qualifiers, + graphql_name: Some(graphql_name), + struct_id, + rust_name, + flatten: false, + deprecation: schema_field.deprecation(), + boxed: false, + }); + } + TypeId::Object(_) | TypeId::Interface(_) | TypeId::Union(_) => { + let struct_name_string = full_path_prefix(*id, context.query); + + context.push_field(ExpandedField { + struct_id, + graphql_name: Some(graphql_name), + rust_name, + field_type_qualifiers: &schema_field.r#type.qualifiers, + field_type: Cow::Owned(struct_name_string.clone()), + flatten: false, + boxed: false, + deprecation: schema_field.deprecation(), + }); + + let type_id = context.push_type(ExpandedType { + name: Cow::Owned(struct_name_string), + }); + + calculate_selection( + context, + selection.subselection(), + type_id, + field_type_id, + options, + ); + } + TypeId::Input(_) => unreachable!("field selection on input type"), + }; + } + Selection::Typename => (), + Selection::InlineFragment(_inline) => (), + Selection::FragmentSpread(fragment_id) => { + // Here we only render fragments that are directly on the type + // itself, and not on one of its variants. + + let fragment = context.query.query.get_fragment(*fragment_id); + + // Assuming the query was validated properly, a fragment spread + // is either on the field's type itself, or on one of the + // variants (union or interfaces). If it's not directly a field + // on the struct, it will be handled in the `on` variants. + if fragment.on != type_id { + continue; + } + + let original_field_name = fragment.name.to_snake_case(); + let final_field_name = keyword_replace(original_field_name); + + context.push_field(ExpandedField { + field_type: fragment.name.as_str().into(), + field_type_qualifiers: &[GraphqlTypeQualifier::Required], + graphql_name: None, + rust_name: final_field_name, + struct_id, + flatten: true, + deprecation: None, + boxed: fragment_is_recursive(*fragment_id, context.query.query), + }); + + // We stop here, because the structs for the fragments are generated separately, to + // avoid duplication. + } + } + } +} + +#[derive(Clone, Copy, PartialEq)] +struct ResponseTypeId(u32); + +struct TypeAlias<'a> { + name: &'a str, + struct_id: ResponseTypeId, + boxed: bool, +} + +struct ExpandedField<'a> { + graphql_name: Option<&'a str>, + rust_name: Cow<'a, str>, + field_type: Cow<'a, str>, + field_type_qualifiers: &'a [GraphqlTypeQualifier], + struct_id: ResponseTypeId, + flatten: bool, + deprecation: Option>, + boxed: bool, +} + +impl ExpandedField<'_> { + fn render(&self, options: &GraphQLClientCodegenOptions) -> Option { + let ident = Ident::new(&self.rust_name, Span::call_site()); + let qualified_type = decorate_type( + &Ident::new(&self.field_type, Span::call_site()), + self.field_type_qualifiers, + ); + + let qualified_type = if self.boxed { + quote!(Box<#qualified_type>) + } else { + qualified_type + }; + + let is_id = self.field_type == "ID"; + let is_required = self + .field_type_qualifiers + .contains(&GraphqlTypeQualifier::Required); + let id_deserialize_with = if is_id && is_required { + Some(quote!(#[serde(deserialize_with = "graphql_client::serde_with::deserialize_id")])) + } else if is_id { + Some( + quote!(#[serde(deserialize_with = "graphql_client::serde_with::deserialize_option_id")]), + ) + } else { + None + }; + + let optional_skip_serializing_none = if *options.skip_serializing_none() + && self + .field_type_qualifiers + .first() + .map(|qualifier| !qualifier.is_required()) + .unwrap_or(false) + { + Some(quote!(#[serde(skip_serializing_if = "Option::is_none")])) + } else { + None + }; + + let optional_rename = self + .graphql_name + .as_ref() + .map(|graphql_name| field_rename_annotation(graphql_name, &self.rust_name)); + let optional_flatten = if self.flatten { + Some(quote!(#[serde(flatten)])) + } else { + None + }; + + let optional_deprecation_annotation = + match (self.deprecation, options.deprecation_strategy()) { + (None, _) | (Some(_), DeprecationStrategy::Allow) => None, + (Some(msg), DeprecationStrategy::Warn) => { + let optional_msg = msg.map(|msg| quote!((note = #msg))); + + Some(quote!(#[deprecated #optional_msg])) + } + (Some(_), DeprecationStrategy::Deny) => return None, + }; + + let tokens = quote! { + #optional_skip_serializing_none + #optional_flatten + #optional_rename + #optional_deprecation_annotation + #id_deserialize_with + pub #ident: #qualified_type + }; + + Some(tokens) + } +} + +struct ExpandedVariant<'a> { + name: Cow<'a, str>, + variant_type: Option>, + on: ResponseTypeId, + is_default_variant: bool, +} + +impl ExpandedVariant<'_> { + fn render(&self) -> TokenStream { + let name_ident = Ident::new(&self.name, Span::call_site()); + let optional_type_ident = self.variant_type.as_ref().map(|variant_type| { + let ident = Ident::new(variant_type, Span::call_site()); + quote!((#ident)) + }); + + if self.is_default_variant { + quote! { + #[serde(other)] + #name_ident #optional_type_ident + } + } else { + quote!(#name_ident #optional_type_ident) + } + } +} + +pub(crate) struct ExpandedType<'a> { + name: Cow<'a, str>, +} + +pub(crate) struct ExpandedSelection<'a> { + query: &'a BoundQuery<'a>, + types: Vec>, + fields: Vec>, + variants: Vec>, + aliases: Vec>, + options: &'a GraphQLClientCodegenOptions, +} + +impl<'a> ExpandedSelection<'a> { + pub(crate) fn schema(&self) -> &'a Schema { + self.query.schema + } + + fn push_type(&mut self, tpe: ExpandedType<'a>) -> ResponseTypeId { + let id = self.types.len(); + self.types.push(tpe); + + ResponseTypeId(id as u32) + } + + fn push_field(&mut self, field: ExpandedField<'a>) { + self.fields.push(field); + } + + fn push_type_alias(&mut self, alias: TypeAlias<'a>) { + self.aliases.push(alias) + } + + fn push_variant(&mut self, variant: ExpandedVariant<'a>) { + self.variants.push(variant); + } + + /// Returns a tuple to be interpreted as (graphql_name, rust_name). + pub(crate) fn field_name(&self, field: &'a SelectedField) -> (&'a str, Cow<'a, str>) { + let name = field + .alias() + .unwrap_or_else(|| &field.schema_field(self.query.schema).name); + let snake_case_name = name.to_snake_case(); + let final_name = keyword_replace(snake_case_name); + + (name, final_name) + } + + fn types(&self) -> impl Iterator)> { + self.types + .iter() + .enumerate() + .map(|(idx, ty)| (ResponseTypeId(idx as u32), ty)) + } + + pub fn render(&self, response_derives: &impl quote::ToTokens) -> TokenStream { + let serde = self.options.serde_path(); + let serde_path = serde.to_token_stream().to_string(); + + let mut items = Vec::with_capacity(self.types.len()); + + for (type_id, ty) in self.types() { + let struct_name = Ident::new(&ty.name, Span::call_site()); + + // If the type is aliased, stop here. + if let Some(alias) = self.aliases.iter().find(|alias| alias.struct_id == type_id) { + let type_name = syn::parse_str::(alias.name).unwrap(); + let type_name = if alias.boxed { + quote!(Box<#type_name>) + } else { + quote!(#type_name) + }; + let item = quote! { + pub type #struct_name = #type_name; + }; + items.push(item); + continue; + } + + let mut fields = self + .fields + .iter() + .filter(|field| field.struct_id == type_id) + .filter_map(|field| field.render(self.options)) + .peekable(); + + let on_variants: Vec = self + .variants + .iter() + .filter(|variant| variant.on == type_id) + .map(|variant| variant.render()) + .collect(); + + // If we only have an `on` field, turn the struct into the enum + // of the variants. + if fields.peek().is_none() { + let item = quote! { + #response_derives + #[serde(tag = "__typename")] + pub enum #struct_name { + #(#on_variants),* + } + }; + items.push(item); + continue; + } + + let (on_field, on_enum) = if !on_variants.is_empty() { + let enum_name = Ident::new(&format!("{}On", ty.name), Span::call_site()); + + let on_field = quote!(#[serde(flatten)] pub on: #enum_name); + + let on_enum = quote!( + #response_derives + #[serde(tag = "__typename")] + pub enum #enum_name { + #(#on_variants,)* + } + ); + + (Some(on_field), Some(on_enum)) + } else { + (None, None) + }; + + let tokens = quote! { + #response_derives + #[serde(crate = #serde_path)] + pub struct #struct_name { + #(#fields,)* + #on_field + } + + #on_enum + }; + + items.push(tokens); + } + + quote!(#(#items)*) + } +} diff --git a/graphql_client_codegen/src/codegen/shared.rs b/graphql_client_codegen/src/codegen/shared.rs new file mode 100644 index 000000000..9bffe3bf1 --- /dev/null +++ b/graphql_client_codegen/src/codegen/shared.rs @@ -0,0 +1,44 @@ +use proc_macro2::TokenStream; +use quote::quote; +use std::borrow::Cow; + +// List of keywords based on https://doc.rust-lang.org/reference/keywords.html +// code snippet: `[...new Set($$("code.hljs").map(x => x.textContent).filter(x => x.match(/^[_a-z0-9]+$/i)))].sort()` +const RUST_KEYWORDS: &[&str] = &[ + "Self", "abstract", "as", "async", "await", "become", "box", "break", "const", "continue", + "crate", "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "if", "impl", + "in", "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub", "ref", + "return", "self", "static", "struct", "super", "trait", "true", "try", "type", "typeof", + "union", "unsafe", "unsized", "use", "virtual", "where", "while", "yield", +]; + +pub(crate) fn keyword_replace<'a>(needle: impl Into>) -> Cow<'a, str> { + let needle = needle.into(); + match RUST_KEYWORDS.binary_search(&needle.as_ref()) { + Ok(index) => [RUST_KEYWORDS[index], "_"].concat().into(), + Err(_) => needle, + } +} + +/// Given the GraphQL schema name for an object/interface/input object field and +/// the equivalent rust name, produces a serde annotation to map them during +/// (de)serialization if it is necessary, otherwise an empty TokenStream. +pub(crate) fn field_rename_annotation(graphql_name: &str, rust_name: &str) -> Option { + if graphql_name != rust_name { + Some(quote!(#[serde(rename = #graphql_name)])) + } else { + None + } +} + +#[cfg(test)] +mod tests { + #[test] + fn keyword_replace_works() { + use super::keyword_replace; + assert_eq!("fora", keyword_replace("fora")); + assert_eq!("in_", keyword_replace("in")); + assert_eq!("fn_", keyword_replace("fn")); + assert_eq!("struct_", keyword_replace("struct")); + } +} diff --git a/graphql_client_codegen/src/codegen_options.rs b/graphql_client_codegen/src/codegen_options.rs index 1fa4cebeb..7b3d8d735 100644 --- a/graphql_client_codegen/src/codegen_options.rs +++ b/graphql_client_codegen/src/codegen_options.rs @@ -1,7 +1,8 @@ use crate::deprecation::DeprecationStrategy; +use crate::normalization::Normalization; use proc_macro2::Ident; use std::path::{Path, PathBuf}; -use syn::Visibility; +use syn::{self, Visibility}; /// Which context is this code generation effort taking place. #[derive(Debug)] @@ -18,12 +19,14 @@ pub struct GraphQLClientCodegenOptions { pub mode: CodegenMode, /// Name of the operation we want to generate code for. If it does not match, we use all queries. pub operation_name: Option, - /// The name of implemention target struct. + /// The name of implementation target struct. pub struct_name: Option, /// The struct for which we derive GraphQLQuery. struct_ident: Option, - /// Comma-separated list of additional traits we want to derive. - additional_derives: Option, + /// Comma-separated list of additional traits we want to derive for variables. + variables_derives: Option, + /// Comma-separated list of additional traits we want to derive for responses. + response_derives: Option, /// The deprecation strategy to adopt. deprecation_strategy: Option, /// Target module visibility. @@ -34,6 +37,22 @@ pub struct GraphQLClientCodegenOptions { /// A path to a file to include in the module to force Cargo to take into account changes in /// the schema files when recompiling. schema_file: Option, + /// Normalization pattern for query types and names. + normalization: Normalization, + /// Custom scalar definitions module path + custom_scalars_module: Option, + /// List of externally defined enum types. Type names must match those used in the schema exactly. + extern_enums: Vec, + /// Flag to trigger generation of Other variant for fragments Enum + fragments_other_variant: bool, + /// Skip Serialization of None values. + skip_serializing_none: bool, + /// Path to the serde crate. + serde_path: syn::Path, + /// list of custom type paths to use for input variables + custom_variable_types: Option>, + /// Custom response type path + custom_response_type: Option, } impl GraphQLClientCodegenOptions { @@ -41,7 +60,8 @@ impl GraphQLClientCodegenOptions { pub fn new(mode: CodegenMode) -> GraphQLClientCodegenOptions { GraphQLClientCodegenOptions { mode, - additional_derives: Default::default(), + variables_derives: Default::default(), + response_derives: Default::default(), deprecation_strategy: Default::default(), module_visibility: Default::default(), operation_name: Default::default(), @@ -49,6 +69,14 @@ impl GraphQLClientCodegenOptions { struct_name: Default::default(), query_file: Default::default(), schema_file: Default::default(), + normalization: Normalization::None, + custom_scalars_module: Default::default(), + extern_enums: Default::default(), + fragments_other_variant: Default::default(), + skip_serializing_none: Default::default(), + serde_path: syn::parse_quote!(::serde), + custom_variable_types: Default::default(), + custom_response_type: Default::default(), } } @@ -70,14 +98,70 @@ impl GraphQLClientCodegenOptions { self.query_file = Some(path); } - /// Comma-separated list of additional traits we want to derive. - pub fn additional_derives(&self) -> Option<&str> { - self.additional_derives.as_ref().map(String::as_str) + /// Comma-separated list of additional traits we want to derive for variables. + pub fn variables_derives(&self) -> Option<&str> { + self.variables_derives.as_deref() } - /// Comma-separated list of additional traits we want to derive. - pub fn set_additional_derives(&mut self, additional_derives: String) { - self.additional_derives = Some(additional_derives); + /// Comma-separated list of additional traits we want to derive for variables. + pub fn set_variables_derives(&mut self, variables_derives: String) { + self.variables_derives = Some(variables_derives); + } + + /// All the variable derives to be rendered. + pub fn all_variable_derives(&self) -> impl Iterator { + let additional = self + .variables_derives + .as_deref() + .into_iter() + .flat_map(|s| s.split(',')) + .map(|s| s.trim()); + + std::iter::once("Serialize").chain(additional) + } + + /// Traits we want to derive for responses. + pub fn all_response_derives(&self) -> impl Iterator { + let base_derives = std::iter::once("Deserialize"); + + base_derives.chain( + self.additional_response_derives() + .filter(|additional| additional != &"Deserialize"), + ) + } + + /// Additional traits we want to derive for responses. + pub fn additional_response_derives(&self) -> impl Iterator { + self.response_derives + .as_deref() + .into_iter() + .flat_map(|s| s.split(',')) + .map(|s| s.trim()) + } + + /// Comma-separated list of additional traits we want to derive for responses. + pub fn set_response_derives(&mut self, response_derives: String) { + self.response_derives = Some(response_derives); + } + + /// Type use as the response type + pub fn custom_response_type(&self) -> Option<&String> { + self.custom_response_type.as_ref() + } + + /// Type use as the response type + pub fn set_custom_response_type(&mut self, response_type: String) { + self.custom_response_type = Some(response_type); + } + + /// list of custom type paths to use for input variables + pub fn custom_variable_types(&self) -> Vec { + self.custom_variable_types.clone().unwrap_or_default() + } + + /// list of custom type paths to use for input variables + pub fn set_custom_variable_types(&mut self, variables_types: Vec) { + self.custom_variable_types = Some(variables_types); } /// The deprecation strategy to adopt. @@ -90,7 +174,7 @@ impl GraphQLClientCodegenOptions { self.module_visibility = Some(visibility); } - /// The name of implemention target struct. + /// The name of implementation target struct. pub fn set_struct_name(&mut self, struct_name: String) { self.struct_name = Some(struct_name); } @@ -104,13 +188,13 @@ impl GraphQLClientCodegenOptions { /// A path to a file to include in the module to force Cargo to take into account changes in /// the schema files when recompiling. pub fn schema_file(&self) -> Option<&Path> { - self.schema_file.as_ref().map(PathBuf::as_path) + self.schema_file.as_deref() } /// A path to a file to include in the module to force Cargo to take into account changes in /// the query files when recompiling. pub fn query_file(&self) -> Option<&Path> { - self.query_file.as_ref().map(PathBuf::as_path) + self.query_file.as_deref() } /// The identifier to use when referring to the struct implementing GraphQLQuery, if any. @@ -122,4 +206,64 @@ impl GraphQLClientCodegenOptions { pub fn struct_ident(&self) -> Option<&proc_macro2::Ident> { self.struct_ident.as_ref() } + + /// Set the normalization mode for the generated code. + pub fn set_normalization(&mut self, norm: Normalization) { + self.normalization = norm; + } + + /// The normalization mode for the generated code. + pub fn normalization(&self) -> &Normalization { + &self.normalization + } + + /// Get the custom scalar definitions module + pub fn custom_scalars_module(&self) -> Option<&syn::Path> { + self.custom_scalars_module.as_ref() + } + + /// Set the custom scalar definitions module + pub fn set_custom_scalars_module(&mut self, module: syn::Path) { + self.custom_scalars_module = Some(module) + } + + /// Get the externally defined enums type names + pub fn extern_enums(&self) -> &[String] { + &self.extern_enums + } + + /// Set the externally defined enums type names + pub fn set_extern_enums(&mut self, enums: Vec) { + self.extern_enums = enums; + } + + /// Set the graphql client codegen options's fragments other variant. + pub fn set_fragments_other_variant(&mut self, fragments_other_variant: bool) { + self.fragments_other_variant = fragments_other_variant; + } + + /// Get a reference to the graphql client codegen options's fragments other variant. + pub fn fragments_other_variant(&self) -> &bool { + &self.fragments_other_variant + } + + /// Set the graphql client codegen option's skip none value. + pub fn set_skip_serializing_none(&mut self, skip_serializing_none: bool) { + self.skip_serializing_none = skip_serializing_none + } + + /// Get a reference to the graphql client codegen option's skip none value. + pub fn skip_serializing_none(&self) -> &bool { + &self.skip_serializing_none + } + + /// Set the path to used to resolve serde traits. + pub fn set_serde_path(&mut self, path: syn::Path) { + self.serde_path = path; + } + + /// Get a reference to the path used to resolve serde traits. + pub fn serde_path(&self) -> &syn::Path { + &self.serde_path + } } diff --git a/graphql_client_codegen/src/constants.rs b/graphql_client_codegen/src/constants.rs index 5fc1ac1da..c051347fc 100644 --- a/graphql_client_codegen/src/constants.rs +++ b/graphql_client_codegen/src/constants.rs @@ -1,29 +1,5 @@ -use crate::deprecation::DeprecationStatus; -use crate::field_type::FieldType; -use crate::objects::GqlObjectField; - pub(crate) const TYPENAME_FIELD: &str = "__typename"; -pub(crate) fn string_type() -> &'static str { - "String" -} - -#[cfg(test)] -pub(crate) fn float_type() -> &'static str { - "Float" -} - -pub(crate) fn typename_field() -> GqlObjectField<'static> { - GqlObjectField { - description: None, - name: TYPENAME_FIELD, - /// Non-nullable, see spec: - /// https://github.com/facebook/graphql/blob/master/spec/Section%204%20--%20Introspection.md - type_: FieldType::new(string_type()), - deprecation: DeprecationStatus::Current, - } -} - pub(crate) const MULTIPLE_SUBSCRIPTION_FIELDS_ERROR: &str = r##" Multiple-field queries on the root subscription field are forbidden by the spec. diff --git a/graphql_client_codegen/src/deprecation.rs b/graphql_client_codegen/src/deprecation.rs index 27b2a74a4..4c5566c2d 100644 --- a/graphql_client_codegen/src/deprecation.rs +++ b/graphql_client_codegen/src/deprecation.rs @@ -1,5 +1,5 @@ /// Whether an item is deprecated, with context. -#[derive(Debug, PartialEq, Hash, Clone)] +#[derive(Debug, PartialEq, Eq, Hash, Clone)] pub enum DeprecationStatus { /// Not deprecated Current, @@ -8,22 +8,17 @@ pub enum DeprecationStatus { } /// The available deprecation strategies. -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Default)] pub enum DeprecationStrategy { /// Allow use of deprecated items in queries, and say nothing. Allow, /// Fail compilation if a deprecated item is used. Deny, /// Allow use of deprecated items in queries, but warn about them (default). + #[default] Warn, } -impl Default for DeprecationStrategy { - fn default() -> Self { - DeprecationStrategy::Warn - } -} - impl std::str::FromStr for DeprecationStrategy { type Err = (); diff --git a/graphql_client_codegen/src/enums.rs b/graphql_client_codegen/src/enums.rs deleted file mode 100644 index d294cc8c0..000000000 --- a/graphql_client_codegen/src/enums.rs +++ /dev/null @@ -1,81 +0,0 @@ -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use std::cell::Cell; - -pub const ENUMS_PREFIX: &str = ""; - -#[derive(Debug, Clone, PartialEq)] -pub struct EnumVariant<'schema> { - pub description: Option<&'schema str>, - pub name: &'schema str, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct GqlEnum<'schema> { - pub description: Option<&'schema str>, - pub name: &'schema str, - pub variants: Vec>, - pub is_required: Cell, -} - -impl<'schema> GqlEnum<'schema> { - pub(crate) fn to_rust( - &self, - query_context: &crate::query::QueryContext<'_, '_>, - ) -> TokenStream { - let derives = query_context.response_enum_derives(); - let variant_names: Vec = self - .variants - .iter() - .map(|v| { - let name = Ident::new(&v.name, Span::call_site()); - let description = &v.description; - let description = description.as_ref().map(|d| quote!(#[doc = #d])); - quote!(#description #name) - }) - .collect(); - let variant_names = &variant_names; - let name_ident = Ident::new(&format!("{}{}", ENUMS_PREFIX, self.name), Span::call_site()); - let constructors: Vec<_> = self - .variants - .iter() - .map(|v| { - let v = Ident::new(&v.name, Span::call_site()); - quote!(#name_ident::#v) - }) - .collect(); - let constructors = &constructors; - let variant_str: Vec<&str> = self.variants.iter().map(|v| v.name).collect(); - let variant_str = &variant_str; - - let name = name_ident.clone(); - - quote! { - #derives - pub enum #name { - #(#variant_names,)* - Other(String), - } - - impl ::serde::Serialize for #name { - fn serialize(&self, ser: S) -> Result { - ser.serialize_str(match *self { - #(#constructors => #variant_str,)* - #name::Other(ref s) => &s, - }) - } - } - - impl<'de> ::serde::Deserialize<'de> for #name { - fn deserialize>(deserializer: D) -> Result { - let s = ::deserialize(deserializer)?; - - match s.as_str() { - #(#variant_str => Ok(#constructors),)* - _ => Ok(#name::Other(s)), - } - } - } - } - } -} diff --git a/graphql_client_codegen/src/field_type.rs b/graphql_client_codegen/src/field_type.rs deleted file mode 100644 index 9b7c73c53..000000000 --- a/graphql_client_codegen/src/field_type.rs +++ /dev/null @@ -1,279 +0,0 @@ -use crate::enums::ENUMS_PREFIX; -use crate::query::QueryContext; -use crate::schema::DEFAULT_SCALARS; -use graphql_introspection_query::introspection_response; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; - -#[derive(Clone, Debug, PartialEq, Hash)] -enum GraphqlTypeQualifier { - Required, - List, -} - -#[derive(Clone, Debug, PartialEq, Hash)] -pub struct FieldType<'a> { - /// The type name of the field. - /// - /// e.g. for `[Int]!`, this would return `Int`. - name: &'a str, - /// An ordered list of qualifiers, from outer to inner. - /// - /// e.g. `[Int]!` would have `vec![List, Optional]`, but `[Int!]` would have `vec![Optional, - /// List]`. - qualifiers: Vec, -} - -impl<'a> FieldType<'a> { - pub(crate) fn new(name: &'a str) -> Self { - FieldType { - name, - qualifiers: Vec::new(), - } - } - - #[cfg(test)] - pub(crate) fn list(mut self) -> Self { - self.qualifiers.insert(0, GraphqlTypeQualifier::List); - self - } - - #[cfg(test)] - pub(crate) fn nonnull(mut self) -> Self { - self.qualifiers.insert(0, GraphqlTypeQualifier::Required); - self - } - - /// Takes a field type with its name. - pub(crate) fn to_rust(&self, context: &QueryContext<'_, '_>, prefix: &str) -> TokenStream { - let prefix: &str = if prefix.is_empty() { - self.inner_name_str() - } else { - prefix - }; - - let full_name = { - if context - .schema - .scalars - .get(&self.name) - .map(|s| s.is_required.set(true)) - .is_some() - || DEFAULT_SCALARS.iter().any(|elem| elem == &self.name) - { - self.name.to_string() - } else if context - .schema - .enums - .get(&self.name) - .map(|enm| enm.is_required.set(true)) - .is_some() - { - format!("{}{}", ENUMS_PREFIX, self.name) - } else { - if prefix.is_empty() { - panic!("Empty prefix for {:?}", self); - } - prefix.to_string() - } - }; - - let full_name = Ident::new(&full_name, Span::call_site()); - let mut qualified = quote!(#full_name); - - let mut non_null = false; - - // Note: we iterate over qualifiers in reverse because it is more intuitive. This - // means we start from the _inner_ type and make our way to the outside. - for qualifier in self.qualifiers.iter().rev() { - match (non_null, qualifier) { - // We are in non-null context, and we wrap the non-null type into a list. - // We switch back to null context. - (true, GraphqlTypeQualifier::List) => { - qualified = quote!(Vec<#qualified>); - non_null = false; - } - // We are in nullable context, and we wrap the nullable type into a list. - (false, GraphqlTypeQualifier::List) => { - qualified = quote!(Vec>); - } - // We are in non-nullable context, but we can't double require a type - // (!!). - (true, GraphqlTypeQualifier::Required) => panic!("double required annotation"), - // We are in nullable context, and we switch to non-nullable context. - (false, GraphqlTypeQualifier::Required) => { - non_null = true; - } - } - } - - // If we are in nullable context at the end of the iteration, we wrap the whole - // type with an Option. - if !non_null { - qualified = quote!(Option<#qualified>); - } - - qualified - } - - /// Return the innermost name - we mostly use this for looking types up in our Schema struct. - pub fn inner_name_str(&self) -> &str { - self.name - } - - /// Is the type nullable? - /// - /// Note: a list of nullable values is considered nullable only if the list itself is nullable. - pub fn is_optional(&self) -> bool { - if let Some(qualifier) = self.qualifiers.get(0) { - qualifier != &GraphqlTypeQualifier::Required - } else { - true - } - } - - /// A type is indirected if it is a (flat or nested) list type, optional or not. - /// - /// We use this to determine whether a type needs to be boxed for recursion. - pub fn is_indirected(&self) -> bool { - self.qualifiers - .iter() - .any(|qualifier| qualifier == &GraphqlTypeQualifier::List) - } -} - -impl<'schema> std::convert::From<&'schema graphql_parser::schema::Type> for FieldType<'schema> { - fn from(schema_type: &'schema graphql_parser::schema::Type) -> FieldType<'schema> { - from_schema_type_inner(schema_type) - } -} - -fn graphql_parser_depth(schema_type: &graphql_parser::schema::Type) -> usize { - match schema_type { - graphql_parser::schema::Type::ListType(inner) => 1 + graphql_parser_depth(inner), - graphql_parser::schema::Type::NonNullType(inner) => 1 + graphql_parser_depth(inner), - graphql_parser::schema::Type::NamedType(_) => 0, - } -} - -fn from_schema_type_inner(inner: &graphql_parser::schema::Type) -> FieldType<'_> { - use graphql_parser::schema::Type::*; - - let qualifiers_depth = graphql_parser_depth(inner); - let mut qualifiers = Vec::with_capacity(qualifiers_depth); - - let mut inner = inner; - - loop { - match inner { - ListType(new_inner) => { - qualifiers.push(GraphqlTypeQualifier::List); - inner = new_inner; - } - NonNullType(new_inner) => { - qualifiers.push(GraphqlTypeQualifier::Required); - inner = new_inner; - } - NamedType(name) => return FieldType { name, qualifiers }, - } - } -} - -fn json_type_qualifiers_depth(typeref: &introspection_response::TypeRef) -> usize { - use graphql_introspection_query::introspection_response::*; - - match (typeref.kind.as_ref(), typeref.of_type.as_ref()) { - (Some(__TypeKind::NON_NULL), Some(inner)) => 1 + json_type_qualifiers_depth(inner), - (Some(__TypeKind::LIST), Some(inner)) => 1 + json_type_qualifiers_depth(inner), - (Some(_), None) => 0, - _ => panic!("Non-convertible type in JSON schema: {:?}", typeref), - } -} - -fn from_json_type_inner(inner: &introspection_response::TypeRef) -> FieldType<'_> { - use graphql_introspection_query::introspection_response::*; - - let qualifiers_depth = json_type_qualifiers_depth(inner); - let mut qualifiers = Vec::with_capacity(qualifiers_depth); - - let mut inner = inner; - - loop { - match ( - inner.kind.as_ref(), - inner.of_type.as_ref(), - inner.name.as_ref(), - ) { - (Some(__TypeKind::NON_NULL), Some(new_inner), _) => { - qualifiers.push(GraphqlTypeQualifier::Required); - inner = &new_inner; - } - (Some(__TypeKind::LIST), Some(new_inner), _) => { - qualifiers.push(GraphqlTypeQualifier::List); - inner = &new_inner; - } - (Some(_), None, Some(name)) => return FieldType { name, qualifiers }, - _ => panic!("Non-convertible type in JSON schema: {:?}", inner), - } - } -} - -impl<'schema> std::convert::From<&'schema introspection_response::FullTypeFieldsType> - for FieldType<'schema> -{ - fn from( - schema_type: &'schema introspection_response::FullTypeFieldsType, - ) -> FieldType<'schema> { - from_json_type_inner(&schema_type.type_ref) - } -} - -impl<'a> std::convert::From<&'a introspection_response::InputValueType> for FieldType<'a> { - fn from(schema_type: &'a introspection_response::InputValueType) -> FieldType<'a> { - from_json_type_inner(&schema_type.type_ref) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use graphql_introspection_query::introspection_response::{ - FullTypeFieldsType, TypeRef, __TypeKind, - }; - use graphql_parser::schema::Type as GqlParserType; - - #[test] - fn field_type_from_graphql_parser_schema_type_works() { - let ty = GqlParserType::NamedType("Cat".to_owned()); - assert_eq!(FieldType::from(&ty), FieldType::new("Cat")); - - let ty = GqlParserType::NonNullType(Box::new(GqlParserType::NamedType("Cat".to_owned()))); - - assert_eq!(FieldType::from(&ty), FieldType::new("Cat").nonnull()); - } - - #[test] - fn field_type_from_introspection_response_works() { - let ty = FullTypeFieldsType { - type_ref: TypeRef { - kind: Some(__TypeKind::OBJECT), - name: Some("Cat".into()), - of_type: None, - }, - }; - assert_eq!(FieldType::from(&ty), FieldType::new("Cat")); - - let ty = FullTypeFieldsType { - type_ref: TypeRef { - kind: Some(__TypeKind::NON_NULL), - name: None, - of_type: Some(Box::new(TypeRef { - kind: Some(__TypeKind::OBJECT), - name: Some("Cat".into()), - of_type: None, - })), - }, - }; - assert_eq!(FieldType::from(&ty), FieldType::new("Cat").nonnull()); - } -} diff --git a/graphql_client_codegen/src/fragments.rs b/graphql_client_codegen/src/fragments.rs deleted file mode 100644 index cd4f33c19..000000000 --- a/graphql_client_codegen/src/fragments.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::query::QueryContext; -use crate::selection::Selection; -use proc_macro2::TokenStream; -use std::cell::Cell; - -/// Represents which type a fragment is defined on. This is the type mentioned in the fragment's `on` clause. -#[derive(Debug, PartialEq)] -pub(crate) enum FragmentTarget<'context> { - Object(&'context crate::objects::GqlObject<'context>), - Interface(&'context crate::interfaces::GqlInterface<'context>), - Union(&'context crate::unions::GqlUnion<'context>), -} - -impl<'context> FragmentTarget<'context> { - pub(crate) fn name(&self) -> &str { - match self { - FragmentTarget::Object(obj) => obj.name, - FragmentTarget::Interface(iface) => iface.name, - FragmentTarget::Union(unn) => unn.name, - } - } -} - -/// Represents a fragment extracted from a query document. -#[derive(Debug, PartialEq)] -pub(crate) struct GqlFragment<'query> { - /// The name of the fragment, matching one-to-one with the name in the GraphQL query document. - pub name: &'query str, - /// The `on` clause of the fragment. - pub on: FragmentTarget<'query>, - /// The selected fields. - pub selection: Selection<'query>, - /// Whether the fragment is used in the current query - pub is_required: Cell, -} - -impl<'query> GqlFragment<'query> { - /// Generate all the Rust code required by the fragment's object selection. - pub(crate) fn to_rust( - &self, - context: &QueryContext<'_, '_>, - ) -> Result { - match self.on { - FragmentTarget::Object(obj) => { - obj.response_for_selection(context, &self.selection, &self.name) - } - FragmentTarget::Interface(iface) => { - iface.response_for_selection(context, &self.selection, &self.name) - } - FragmentTarget::Union(_) => { - unreachable!("Wrong code path. Fragment on unions are treated differently.") - } - } - } - - pub(crate) fn is_recursive(&self) -> bool { - self.selection.contains_fragment(&self.name) - } -} diff --git a/graphql_client_codegen/src/generated_module.rs b/graphql_client_codegen/src/generated_module.rs index a093a9c89..b225d001a 100644 --- a/graphql_client_codegen/src/generated_module.rs +++ b/graphql_client_codegen/src/generated_module.rs @@ -1,34 +1,67 @@ -use crate::codegen_options::*; +use crate::{ + codegen_options::*, + query::{BoundQuery, OperationId}, + BoxError, +}; use heck::*; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; +use std::{error::Error, fmt::Display}; + +#[derive(Debug)] +struct OperationNotFound { + operation_name: String, +} + +impl Display for OperationNotFound { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Could not find an operation named ")?; + f.write_str(&self.operation_name)?; + f.write_str(" in the query document.") + } +} + +impl Error for OperationNotFound {} /// This struct contains the parameters necessary to generate code for a given operation. pub(crate) struct GeneratedModule<'a> { - pub operation: &'a crate::operations::Operation<'a>, + pub operation: &'a str, pub query_string: &'a str, - pub query_document: &'a graphql_parser::query::Document, - pub schema: &'a crate::schema::Schema<'a>, + pub resolved_query: &'a crate::query::Query, + pub schema: &'a crate::schema::Schema, pub options: &'a crate::GraphQLClientCodegenOptions, } -impl<'a> GeneratedModule<'a> { +impl GeneratedModule<'_> { /// Generate the items for the variables and the response that will go inside the module. - fn build_impls(&self) -> Result { + fn build_impls(&self) -> Result { Ok(crate::codegen::response_for_query( - &self.schema, - &self.query_document, - &self.operation, - &self.options, + self.root()?, + self.options, + BoundQuery { + query: self.resolved_query, + schema: self.schema, + }, )?) } + fn root(&self) -> Result { + let op_name = self.options.normalization().operation(self.operation); + self.resolved_query + .select_operation(&op_name, *self.options.normalization()) + .map(|op| op.0) + .ok_or_else(|| OperationNotFound { + operation_name: op_name.into(), + }) + } + /// Generate the module and all the code inside. - pub(crate) fn to_token_stream(&self) -> Result { - let module_name = Ident::new(&self.operation.name.to_snake_case(), Span::call_site()); + pub(crate) fn to_token_stream(&self) -> Result { + let module_name = Ident::new(&self.operation.to_snake_case(), Span::call_site()); let module_visibility = &self.options.module_visibility(); - let operation_name_literal = &self.operation.name; - let operation_name_ident = Ident::new(&self.operation.name, Span::call_site()); + let operation_name = self.operation; + let operation_name_ident = self.options.normalization().operation(self.operation); + let operation_name_ident = Ident::new(&operation_name_ident, Span::call_site()); // Force cargo to refresh the generated code when the query file changes. let query_include = self @@ -40,7 +73,7 @@ impl<'a> GeneratedModule<'a> { const __QUERY_WORKAROUND: &str = include_str!(#path); ) }) - .unwrap_or_else(|| quote! {}); + .unwrap_or_default(); let query_string = &self.query_string; let impls = self.build_impls()?; @@ -57,8 +90,10 @@ impl<'a> GeneratedModule<'a> { #module_visibility mod #module_name { #![allow(dead_code)] - pub const OPERATION_NAME: &'static str = #operation_name_literal; - pub const QUERY: &'static str = #query_string; + use std::result::Result; + + pub const OPERATION_NAME: &str = #operation_name; + pub const QUERY: &str = #query_string; #query_include @@ -69,7 +104,7 @@ impl<'a> GeneratedModule<'a> { type Variables = #module_name::Variables; type ResponseData = #module_name::ResponseData; - fn build_query(variables: Self::Variables) -> ::graphql_client::QueryBody { + fn build_query(variables: Self::Variables) -> graphql_client::QueryBody { graphql_client::QueryBody { variables, query: #module_name::QUERY, diff --git a/graphql_client_codegen/src/inputs.rs b/graphql_client_codegen/src/inputs.rs deleted file mode 100644 index 0dc0f49b9..000000000 --- a/graphql_client_codegen/src/inputs.rs +++ /dev/null @@ -1,243 +0,0 @@ -use crate::deprecation::DeprecationStatus; -use crate::objects::GqlObjectField; -use crate::query::QueryContext; -use crate::schema::Schema; -use graphql_introspection_query::introspection_response; -use heck::SnakeCase; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use std::cell::Cell; -use std::collections::HashMap; - -/// Represents an input object type from a GraphQL schema -#[derive(Debug, Clone, PartialEq)] -pub struct GqlInput<'schema> { - pub description: Option<&'schema str>, - pub name: &'schema str, - pub fields: HashMap<&'schema str, GqlObjectField<'schema>>, - pub is_required: Cell, -} - -impl<'schema> GqlInput<'schema> { - pub(crate) fn require(&self, schema: &Schema<'schema>) { - if self.is_required.get() { - return; - } - self.is_required.set(true); - self.fields.values().for_each(|field| { - schema.require(&field.type_.inner_name_str()); - }) - } - - fn contains_type_without_indirection( - &self, - context: &QueryContext<'_, '_>, - type_name: &str, - ) -> bool { - // the input type is recursive if any of its members contains it, without indirection - self.fields.values().any(|field| { - // the field is indirected, so no boxing is needed - if field.type_.is_indirected() { - return false; - } - - let field_type_name = field.type_.inner_name_str(); - let input = context.schema.inputs.get(field_type_name); - - if let Some(input) = input { - // the input contains itself, not indirected - if input.name == type_name { - return true; - } - - // we check if the other input contains this one (without indirection) - input.contains_type_without_indirection(context, type_name) - } else { - // the field is not referring to an input type - false - } - }) - } - - fn is_recursive_without_indirection(&self, context: &QueryContext<'_, '_>) -> bool { - self.contains_type_without_indirection(context, &self.name) - } - - pub(crate) fn to_rust( - &self, - context: &QueryContext<'_, '_>, - ) -> Result { - let name = Ident::new(&self.name, Span::call_site()); - let mut fields: Vec<&GqlObjectField<'_>> = self.fields.values().collect(); - fields.sort_unstable_by(|a, b| a.name.cmp(&b.name)); - let fields = fields.iter().map(|field| { - let ty = field.type_.to_rust(&context, ""); - - // If the type is recursive, we have to box it - let ty = if let Some(input) = context.schema.inputs.get(field.type_.inner_name_str()) { - if input.is_recursive_without_indirection(context) { - quote! { Box<#ty> } - } else { - quote!(#ty) - } - } else { - quote!(#ty) - }; - - context.schema.require(&field.type_.inner_name_str()); - let original_name = &field.name; - let snake_case_name = field.name.to_snake_case(); - let rename = crate::shared::field_rename_annotation(&original_name, &snake_case_name); - let name = Ident::new(&snake_case_name, Span::call_site()); - - quote!(#rename pub #name: #ty) - }); - let variables_derives = context.variables_derives(); - - Ok(quote! { - #variables_derives - pub struct #name { - #(#fields,)* - } - }) - } -} - -impl<'schema> std::convert::From<&'schema graphql_parser::schema::InputObjectType> - for GqlInput<'schema> -{ - fn from(schema_input: &'schema graphql_parser::schema::InputObjectType) -> GqlInput<'schema> { - GqlInput { - description: schema_input.description.as_ref().map(String::as_str), - name: &schema_input.name, - fields: schema_input - .fields - .iter() - .map(|field| { - let name = field.name.as_str(); - let field = GqlObjectField { - description: None, - name: &field.name, - type_: crate::field_type::FieldType::from(&field.value_type), - deprecation: DeprecationStatus::Current, - }; - (name, field) - }) - .collect(), - is_required: false.into(), - } - } -} - -impl<'schema> std::convert::From<&'schema introspection_response::FullType> for GqlInput<'schema> { - fn from(schema_input: &'schema introspection_response::FullType) -> GqlInput<'schema> { - GqlInput { - description: schema_input.description.as_ref().map(String::as_str), - name: schema_input - .name - .as_ref() - .map(String::as_str) - .expect("unnamed input object"), - fields: schema_input - .input_fields - .as_ref() - .expect("fields on input object") - .iter() - .filter_map(Option::as_ref) - .map(|f| { - let name = f - .input_value - .name - .as_ref() - .expect("unnamed input object field") - .as_str(); - let field = GqlObjectField { - description: None, - name: &name, - type_: f - .input_value - .type_ - .as_ref() - .map(|s| s.into()) - .expect("type on input object field"), - deprecation: DeprecationStatus::Current, - }; - (name, field) - }) - .collect(), - is_required: false.into(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::constants::*; - use crate::field_type::FieldType; - - #[test] - fn gql_input_to_rust() { - let cat = GqlInput { - description: None, - name: "Cat", - fields: vec![ - ( - "pawsCount", - GqlObjectField { - description: None, - name: "pawsCount", - type_: FieldType::new(float_type()).nonnull(), - deprecation: DeprecationStatus::Current, - }, - ), - ( - "offsprings", - GqlObjectField { - description: None, - name: "offsprings", - type_: FieldType::new("Cat").nonnull().list().nonnull(), - deprecation: DeprecationStatus::Current, - }, - ), - ( - "requirements", - GqlObjectField { - description: None, - name: "requirements", - type_: FieldType::new("CatRequirements"), - deprecation: DeprecationStatus::Current, - }, - ), - ] - .into_iter() - .collect(), - is_required: false.into(), - }; - - let expected: String = vec![ - "# [ derive ( Clone , Serialize ) ] ", - "pub struct Cat { ", - "pub offsprings : Vec < Cat > , ", - "# [ serde ( rename = \"pawsCount\" ) ] ", - "pub paws_count : Float , ", - "pub requirements : Option < CatRequirements > , ", - "}", - ] - .into_iter() - .collect(); - - let mut schema = crate::schema::Schema::new(); - schema.inputs.insert(cat.name, cat); - let mut context = QueryContext::new_empty(&schema); - context.ingest_additional_derives("Clone").unwrap(); - - assert_eq!( - format!( - "{}", - context.schema.inputs["Cat"].to_rust(&context).unwrap() - ), - expected - ); - } -} diff --git a/graphql_client_codegen/src/interfaces.rs b/graphql_client_codegen/src/interfaces.rs deleted file mode 100644 index 9813f4b6e..000000000 --- a/graphql_client_codegen/src/interfaces.rs +++ /dev/null @@ -1,263 +0,0 @@ -use crate::constants::TYPENAME_FIELD; -use crate::objects::GqlObjectField; -use crate::query::QueryContext; -use crate::selection::{Selection, SelectionField, SelectionFragmentSpread, SelectionItem}; -use crate::shared::*; -use crate::unions::union_variants; -use failure::*; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use std::cell::Cell; -use std::collections::HashSet; - -/// A GraphQL interface (simplified schema representation). -/// -/// In the generated code, fragments nesting is preserved, including for selection on union variants. See the tests in the graphql client crate for examples. -#[derive(Debug, Clone, PartialEq)] -pub struct GqlInterface<'schema> { - /// The documentation for the interface. Extracted from the schema. - pub description: Option<&'schema str>, - /// The set of object types implementing this interface. - pub implemented_by: HashSet<&'schema str>, - /// The name of the interface. Should match 1-to-1 to its name in the GraphQL schema. - pub name: &'schema str, - /// The interface's fields. Analogous to object fields. - pub fields: Vec>, - pub is_required: Cell, -} - -impl<'schema> GqlInterface<'schema> { - /// filters the selection to keep only the fields that refer to the interface's own. - /// - /// This does not include the __typename field because it is translated into the `on` enum. - fn object_selection<'query>( - &self, - selection: &'query Selection<'query>, - query_context: &QueryContext<'_, '_>, - ) -> Selection<'query> { - (&selection) - .into_iter() - // Only keep what we can handle - .filter(|f| match f { - SelectionItem::Field(f) => f.name != TYPENAME_FIELD, - SelectionItem::FragmentSpread(SelectionFragmentSpread { fragment_name }) => { - // only if the fragment refers to the interface’s own fields (to take into account type-refining fragments) - let fragment = query_context - .fragments - .get(fragment_name) - .ok_or_else(|| format_err!("Unknown fragment: {}", &fragment_name)) - // TODO: fix this - .unwrap(); - - fragment.on.name() == self.name - } - SelectionItem::InlineFragment(_) => false, - }) - .map(|a| (*a).clone()) - .collect() - } - - fn union_selection<'query>( - &self, - selection: &'query Selection<'_>, - query_context: &QueryContext<'_, '_>, - ) -> Selection<'query> { - (&selection) - .into_iter() - // Only keep what we can handle - .filter(|f| match f { - SelectionItem::InlineFragment(_) => true, - SelectionItem::FragmentSpread(SelectionFragmentSpread { fragment_name }) => { - let fragment = query_context - .fragments - .get(fragment_name) - .ok_or_else(|| format_err!("Unknown fragment: {}", &fragment_name)) - // TODO: fix this - .unwrap(); - - // only the fragments _not_ on the interface - fragment.on.name() != self.name - } - SelectionItem::Field(SelectionField { name, .. }) => *name == "__typename", - }) - .map(|a| (*a).clone()) - .collect() - } - - /// Create an empty interface. This needs to be mutated before it is useful. - pub(crate) fn new( - name: &'schema str, - description: Option<&'schema str>, - ) -> GqlInterface<'schema> { - GqlInterface { - name, - description, - implemented_by: HashSet::new(), - fields: vec![], - is_required: false.into(), - } - } - - /// The generated code for each of the selected field's types. See [shared::field_impls_for_selection]. - pub(crate) fn field_impls_for_selection( - &self, - context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, - ) -> Result, failure::Error> { - crate::shared::field_impls_for_selection( - &self.fields, - context, - &self.object_selection(selection, context), - prefix, - ) - } - - /// The code for the interface's corresponding struct's fields. - pub(crate) fn response_fields_for_selection( - &self, - context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, - ) -> Result, failure::Error> { - response_fields_for_selection( - &self.name, - &self.fields, - context, - &self.object_selection(selection, context), - prefix, - ) - } - - /// Generate all the code for the interface. - pub(crate) fn response_for_selection( - &self, - query_context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, - ) -> Result { - let name = Ident::new(&prefix, Span::call_site()); - let derives = query_context.response_derives(); - - selection.extract_typename(query_context).ok_or_else(|| { - format_err!( - "Missing __typename in selection for the {} interface (type: {})", - prefix, - self.name - ) - })?; - - let object_fields = - self.response_fields_for_selection(query_context, &selection, prefix)?; - - let object_children = self.field_impls_for_selection(query_context, &selection, prefix)?; - - let union_selection = self.union_selection(&selection, &query_context); - - let (mut union_variants, union_children, used_variants) = - union_variants(&union_selection, query_context, prefix, &self.name)?; - - // Add the non-selected variants to the generated enum's variants. - union_variants.extend( - self.implemented_by - .iter() - .filter(|obj| used_variants.iter().find(|v| v == obj).is_none()) - .map(|v| { - let v = Ident::new(v, Span::call_site()); - quote!(#v) - }), - ); - - let attached_enum_name = Ident::new(&format!("{}On", name), Span::call_site()); - let (attached_enum, last_object_field) = - if selection.extract_typename(query_context).is_some() { - let attached_enum = quote! { - #derives - #[serde(tag = "__typename")] - pub enum #attached_enum_name { - #(#union_variants,)* - } - }; - let last_object_field = quote!(#[serde(flatten)] pub on: #attached_enum_name,); - (Some(attached_enum), Some(last_object_field)) - } else { - (None, None) - }; - - Ok(quote! { - - #(#object_children)* - - #(#union_children)* - - #attached_enum - - #derives - pub struct #name { - #(#object_fields,)* - #last_object_field - } - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // to be improved - #[test] - fn union_selection_works() { - let iface = GqlInterface { - description: None, - implemented_by: HashSet::new(), - name: "MyInterface", - fields: vec![], - is_required: Cell::new(true), - }; - - let schema = crate::schema::Schema::new(); - let context = QueryContext::new_empty(&schema); - - let typename_field = - crate::selection::SelectionItem::Field(crate::selection::SelectionField { - alias: None, - name: "__typename", - fields: Selection::new_empty(), - }); - let selection = Selection::from_vec(vec![typename_field.clone()]); - - assert_eq!( - iface.union_selection(&selection, &context), - Selection::from_vec(vec![typename_field]) - ); - } - - // to be improved - #[test] - fn object_selection_works() { - let iface = GqlInterface { - description: None, - implemented_by: HashSet::new(), - name: "MyInterface", - fields: vec![], - is_required: Cell::new(true), - }; - - let schema = crate::schema::Schema::new(); - let context = QueryContext::new_empty(&schema); - - let typename_field = - crate::selection::SelectionItem::Field(crate::selection::SelectionField { - alias: None, - name: "__typename", - fields: Selection::new_empty(), - }); - let selection: Selection<'_> = vec![typename_field].into_iter().collect(); - - assert_eq!( - iface.object_selection(&selection, &context), - Selection::new_empty() - ); - } -} diff --git a/graphql_client_codegen/src/lib.rs b/graphql_client_codegen/src/lib.rs index 407e17e20..8d087057f 100644 --- a/graphql_client_codegen/src/lib.rs +++ b/graphql_client_codegen/src/lib.rs @@ -1,133 +1,163 @@ -#![recursion_limit = "128"] #![deny(missing_docs)] -#![deny(rust_2018_idioms)] -#![deny(warnings)] +#![warn(rust_2018_idioms)] +#![allow(clippy::option_option)] -//! Crate for internal use by other graphql-client crates, for code generation. -//! -//! It is not meant to be used directly by users of the library. +//! Crate for Rust code generation from a GraphQL query, schema, and options. -use failure::*; use lazy_static::*; use proc_macro2::TokenStream; use quote::*; +use schema::Schema; mod codegen; mod codegen_options; /// Deprecation-related code pub mod deprecation; -mod query; /// Contains the [Schema] type and its implementation. pub mod schema; mod constants; -mod enums; -mod field_type; -mod fragments; mod generated_module; -mod inputs; -mod interfaces; -mod objects; -mod operations; -mod scalars; -mod selection; -mod shared; -mod unions; -mod variables; +/// Normalization-related code +pub mod normalization; +mod query; +mod type_qualifiers; #[cfg(test)] mod tests; pub use crate::codegen_options::{CodegenMode, GraphQLClientCodegenOptions}; -use std::collections::HashMap; +use std::{collections::BTreeMap, fmt::Display, io}; + +#[derive(Debug)] +struct GeneralError(String); + +impl Display for GeneralError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl std::error::Error for GeneralError {} -type CacheMap = std::sync::Mutex>; +type BoxError = Box; +type CacheMap = std::sync::Mutex>; +type QueryDocument = graphql_parser::query::Document<'static, String>; lazy_static! { - static ref SCHEMA_CACHE: CacheMap = CacheMap::default(); - static ref QUERY_CACHE: CacheMap<(String, graphql_parser::query::Document)> = - CacheMap::default(); + static ref SCHEMA_CACHE: CacheMap = CacheMap::default(); + static ref QUERY_CACHE: CacheMap<(String, QueryDocument)> = CacheMap::default(); } -/// Generates Rust code given a query document, a schema and options. +fn get_set_cached( + cache: &CacheMap, + key: &std::path::Path, + value_func: impl FnOnce() -> T, +) -> T { + let mut lock = cache.lock().expect("cache is poisoned"); + lock.entry(key.into()).or_insert_with(value_func).clone() +} + +fn query_document(query_string: &str) -> Result { + let document = graphql_parser::parse_query(query_string) + .map_err(|err| GeneralError(format!("Query parser error: {}", err)))? + .into_static(); + Ok(document) +} + +fn get_set_query_from_file(query_path: &std::path::Path) -> (String, QueryDocument) { + get_set_cached(&QUERY_CACHE, query_path, || { + let query_string = read_file(query_path).unwrap(); + let query_document = query_document(&query_string).unwrap(); + (query_string, query_document) + }) +} + +fn get_set_schema_from_file(schema_path: &std::path::Path) -> Schema { + get_set_cached(&SCHEMA_CACHE, schema_path, || { + let schema_extension = schema_path + .extension() + .map(|ext| ext.to_str().expect("Path must be valid UTF-8")) + .unwrap_or(""); + let schema_string = read_file(schema_path).unwrap(); + match schema_extension { + "graphql" | "graphqls"| "gql" => { + let s = graphql_parser::schema::parse_schema::<&str>(&schema_string).map_err(|parser_error| GeneralError(format!("Parser error: {}", parser_error))).unwrap(); + Schema::from(s) + } + "json" => { + let parsed: graphql_introspection_query::introspection_response::IntrospectionResponse = serde_json::from_str(&schema_string).unwrap(); + Schema::from(parsed) + } + extension => panic!("Unsupported extension for the GraphQL schema: {} (only .json, .graphql, .graphqls and .gql are supported)", extension) + } + }) +} + +/// Generates Rust code given a path to a query file, a path to a schema file, and options. pub fn generate_module_token_stream( query_path: std::path::PathBuf, schema_path: &std::path::Path, options: GraphQLClientCodegenOptions, -) -> Result { - use std::collections::hash_map; +) -> Result { + let query = get_set_query_from_file(query_path.as_path()); + let schema = get_set_schema_from_file(schema_path); + + generate_module_token_stream_inner(&query, &schema, options) +} + +/// Generates Rust code given a query string, a path to a schema file, and options. +pub fn generate_module_token_stream_from_string( + query_string: &str, + schema_path: &std::path::Path, + options: GraphQLClientCodegenOptions, +) -> Result { + let query = (query_string.to_string(), query_document(query_string)?); + let schema = get_set_schema_from_file(schema_path); + + generate_module_token_stream_inner(&query, &schema, options) +} + +/// Generates Rust code given a query string and query document, a schema, and options. +fn generate_module_token_stream_inner( + query: &(String, QueryDocument), + schema: &Schema, + options: GraphQLClientCodegenOptions, +) -> Result { + let (query_string, query_document) = query; + // We need to qualify the query with the path to the crate it is part of - let (query_string, query) = { - let mut lock = QUERY_CACHE.lock().expect("query cache is poisoned"); - match lock.entry(query_path) { - hash_map::Entry::Occupied(o) => o.get().clone(), - hash_map::Entry::Vacant(v) => { - let query_string = read_file(v.key())?; - let query = graphql_parser::parse_query(&query_string)?; - v.insert((query_string, query)).clone() - } - } - }; + let query = crate::query::resolve(schema, query_document)?; // Determine which operation we are generating code for. This will be used in operationName. let operations = options .operation_name .as_ref() - .and_then(|operation_name| codegen::select_operation(&query, &operation_name)) + .and_then(|operation_name| query.select_operation(operation_name, *options.normalization())) .map(|op| vec![op]); let operations = match (operations, &options.mode) { (Some(ops), _) => ops, - (None, &CodegenMode::Cli) => codegen::all_operations(&query), + (None, &CodegenMode::Cli) => query.operations().collect(), (None, &CodegenMode::Derive) => { - return Err(derive_operation_not_found_error( + return Err(GeneralError(derive_operation_not_found_error( options.struct_ident(), &query, - )); + )) + .into()); } }; - let schema_extension = schema_path - .extension() - .and_then(std::ffi::OsStr::to_str) - .unwrap_or("INVALID"); - - // Check the schema cache. - let schema_string: String = { - let mut lock = SCHEMA_CACHE.lock().expect("schema cache is poisoned"); - match lock.entry(schema_path.to_path_buf()) { - hash_map::Entry::Occupied(o) => o.get().clone(), - hash_map::Entry::Vacant(v) => { - let schema_string = read_file(v.key())?; - v.insert(schema_string).to_string() - } - } - }; - - let parsed_schema = match schema_extension { - "graphql" | "gql" => { - let s = graphql_parser::schema::parse_schema(&schema_string)?; - schema::ParsedSchema::GraphQLParser(s) - } - "json" => { - let parsed: graphql_introspection_query::introspection_response::IntrospectionResponse = serde_json::from_str(&schema_string)?; - schema::ParsedSchema::Json(parsed) - } - extension => panic!("Unsupported extension for the GraphQL schema: {} (only .json and .graphql are supported)", extension) - }; - - let schema = schema::Schema::from(&parsed_schema); - // The generated modules. let mut modules = Vec::with_capacity(operations.len()); for operation in &operations { let generated = generated_module::GeneratedModule { query_string: query_string.as_str(), - schema: &schema, - query_document: &query, - operation, + schema, + resolved_query: &query, + operation: &operation.1.name, options: &options, } .to_token_stream()?; @@ -139,60 +169,71 @@ pub fn generate_module_token_stream( Ok(modules) } -fn read_file(path: &std::path::Path) -> Result { +#[derive(Debug)] +enum ReadFileError { + FileNotFound { path: String, io_error: io::Error }, + ReadError { path: String, io_error: io::Error }, +} + +impl Display for ReadFileError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReadFileError::FileNotFound { path, .. } => { + write!(f, "Could not find file with path: {}\n + Hint: file paths in the GraphQLQuery attribute are relative to the project root (location of the Cargo.toml). Example: query_path = \"src/my_query.graphql\".", path) + } + ReadFileError::ReadError { path, .. } => { + f.write_str("Error reading file at: ")?; + f.write_str(path) + } + } + } +} + +impl std::error::Error for ReadFileError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + ReadFileError::FileNotFound { io_error, .. } + | ReadFileError::ReadError { io_error, .. } => Some(io_error), + } + } +} + +fn read_file(path: &std::path::Path) -> Result { use std::fs; use std::io::prelude::*; let mut out = String::new(); - let mut file = fs::File::open(path).map_err(|io_err| { - let err: failure::Error = io_err.into(); - err.context(format!( - r#" - Could not find file with path: {} - Hint: file paths in the GraphQLQuery attribute are relative to the project root (location of the Cargo.toml). Example: query_path = "src/my_query.graphql". - "#, - path.display() - )) + let mut file = fs::File::open(path).map_err(|io_error| ReadFileError::FileNotFound { + io_error, + path: path.display().to_string(), })?; - file.read_to_string(&mut out)?; + + file.read_to_string(&mut out) + .map_err(|io_error| ReadFileError::ReadError { + io_error, + path: path.display().to_string(), + })?; Ok(out) } /// In derive mode, build an error when the operation with the same name as the struct is not found. fn derive_operation_not_found_error( ident: Option<&proc_macro2::Ident>, - query: &graphql_parser::query::Document, -) -> failure::Error { - use graphql_parser::query::*; - + query: &crate::query::Query, +) -> String { let operation_name = ident.map(ToString::to_string); - let struct_ident = operation_name.as_ref().map(String::as_str).unwrap_or(""); - - let available_operations = query - .definitions - .iter() - .filter_map(|definition| match definition { - Definition::Operation(op) => match op { - OperationDefinition::Mutation(m) => Some(m.name.as_ref().unwrap()), - OperationDefinition::Query(m) => Some(m.name.as_ref().unwrap()), - OperationDefinition::Subscription(m) => Some(m.name.as_ref().unwrap()), - OperationDefinition::SelectionSet(_) => { - unreachable!("Bare selection sets are not supported.") - } - }, - _ => None, - }) - .fold(String::new(), |mut acc, item| { - acc.push_str(&item); - acc.push_str(", "); - acc - }); - - let available_operations = available_operations.trim_end_matches(", "); - - return format_err!( + let struct_ident = operation_name.as_deref().unwrap_or(""); + + let available_operations: Vec<&str> = query + .operations() + .map(|(_id, op)| op.name.as_str()) + .collect(); + let available_operations: String = available_operations.join(", "); + + format!( "The struct name does not match any defined operation in the query file.\nStruct name: {}\nDefined operations: {}", struct_ident, available_operations, - ); + ) } diff --git a/graphql_client_codegen/src/normalization.rs b/graphql_client_codegen/src/normalization.rs new file mode 100644 index 000000000..90275d4e4 --- /dev/null +++ b/graphql_client_codegen/src/normalization.rs @@ -0,0 +1,64 @@ +use heck::ToUpperCamelCase; +use std::borrow::Cow; + +/// Normalization conventions available for generated code. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum Normalization { + /// Use naming conventions from the schema. + None, + /// Use Rust naming conventions for generated code. + Rust, +} + +impl Normalization { + fn camel_case(self, name: &str) -> Cow<'_, str> { + match self { + Self::None => name.into(), + Self::Rust => name.to_upper_camel_case().into(), + } + } + + pub(crate) fn operation(self, op: &str) -> Cow<'_, str> { + self.camel_case(op) + } + + pub(crate) fn enum_variant(self, enm: &str) -> Cow<'_, str> { + self.camel_case(enm) + } + + pub(crate) fn enum_name(self, enm: &str) -> Cow<'_, str> { + self.camel_case(enm) + } + + fn field_type_impl(self, fty: &str) -> Cow<'_, str> { + if fty == "ID" || fty.starts_with("__") { + fty.into() + } else { + self.camel_case(fty) + } + } + + pub(crate) fn field_type(self, fty: &str) -> Cow<'_, str> { + self.field_type_impl(fty) + } + + pub(crate) fn input_name(self, inm: &str) -> Cow<'_, str> { + self.camel_case(inm) + } + + pub(crate) fn scalar_name(self, snm: &str) -> Cow<'_, str> { + self.camel_case(snm) + } +} + +impl std::str::FromStr for Normalization { + type Err = (); + + fn from_str(s: &str) -> Result { + match s.trim() { + "none" => Ok(Normalization::None), + "rust" => Ok(Normalization::Rust), + _ => Err(()), + } + } +} diff --git a/graphql_client_codegen/src/objects.rs b/graphql_client_codegen/src/objects.rs deleted file mode 100644 index b1ef19b82..000000000 --- a/graphql_client_codegen/src/objects.rs +++ /dev/null @@ -1,233 +0,0 @@ -use crate::constants::*; -use crate::deprecation::DeprecationStatus; -use crate::field_type::FieldType; -use crate::query::QueryContext; -use crate::schema::Schema; -use crate::selection::*; -use crate::shared::{field_impls_for_selection, response_fields_for_selection}; -use graphql_parser::schema; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use std::cell::Cell; - -#[derive(Debug, Clone, PartialEq)] -pub struct GqlObject<'schema> { - pub description: Option<&'schema str>, - pub fields: Vec>, - pub name: &'schema str, - pub is_required: Cell, -} - -#[derive(Clone, Debug, PartialEq, Hash)] -pub struct GqlObjectField<'schema> { - pub description: Option<&'schema str>, - pub name: &'schema str, - pub type_: FieldType<'schema>, - pub deprecation: DeprecationStatus, -} - -fn parse_deprecation_info(field: &schema::Field) -> DeprecationStatus { - let deprecated = field - .directives - .iter() - .filter(|x| x.name.to_lowercase() == "deprecated") - .nth(0); - let reason = if let Some(d) = deprecated { - if let Some((_, value)) = d - .arguments - .iter() - .filter(|x| x.0.to_lowercase() == "reason") - .nth(0) - { - match value { - schema::Value::String(reason) => Some(reason.clone()), - schema::Value::Null => None, - _ => panic!("deprecation reason is not a string"), - } - } else { - None - } - } else { - None - }; - match deprecated { - Some(_) => DeprecationStatus::Deprecated(reason), - None => DeprecationStatus::Current, - } -} - -impl<'schema> GqlObject<'schema> { - pub fn new(name: &'schema str, description: Option<&'schema str>) -> GqlObject<'schema> { - GqlObject { - description, - name, - fields: vec![typename_field()], - is_required: false.into(), - } - } - - pub fn from_graphql_parser_object(obj: &'schema schema::ObjectType) -> Self { - let description = obj.description.as_ref().map(String::as_str); - let mut item = GqlObject::new(&obj.name, description); - item.fields.extend(obj.fields.iter().map(|f| { - let deprecation = parse_deprecation_info(&f); - GqlObjectField { - description: f.description.as_ref().map(String::as_str), - name: &f.name, - type_: FieldType::from(&f.field_type), - deprecation, - } - })); - item - } - - pub fn from_introspected_schema_json( - obj: &'schema graphql_introspection_query::introspection_response::FullType, - ) -> Self { - let description = obj.description.as_ref().map(String::as_str); - let mut item = GqlObject::new(obj.name.as_ref().expect("missing object name"), description); - let fields = obj.fields.as_ref().unwrap().iter().filter_map(|t| { - t.as_ref().map(|t| { - let deprecation = if t.is_deprecated.unwrap_or(false) { - DeprecationStatus::Deprecated(t.deprecation_reason.clone()) - } else { - DeprecationStatus::Current - }; - GqlObjectField { - description: t.description.as_ref().map(String::as_str), - name: t.name.as_ref().expect("field name"), - type_: FieldType::from(t.type_.as_ref().expect("field type")), - deprecation, - } - }) - }); - - item.fields.extend(fields); - - item - } - - pub(crate) fn require(&self, schema: &Schema<'_>) { - if self.is_required.get() { - return; - } - self.is_required.set(true); - self.fields.iter().for_each(|field| { - schema.require(&field.type_.inner_name_str()); - }) - } - - pub(crate) fn response_for_selection( - &self, - query_context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, - ) -> Result { - let derives = query_context.response_derives(); - let name = Ident::new(prefix, Span::call_site()); - let fields = self.response_fields_for_selection(query_context, selection, prefix)?; - let field_impls = self.field_impls_for_selection(query_context, selection, &prefix)?; - let description = self.description.as_ref().map(|desc| quote!(#[doc = #desc])); - Ok(quote! { - #(#field_impls)* - - #derives - #description - pub struct #name { - #(#fields,)* - } - }) - } - - pub(crate) fn field_impls_for_selection( - &self, - query_context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, - ) -> Result, failure::Error> { - field_impls_for_selection(&self.fields, query_context, selection, prefix) - } - - pub(crate) fn response_fields_for_selection( - &self, - query_context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, - ) -> Result, failure::Error> { - response_fields_for_selection(&self.name, &self.fields, query_context, selection, prefix) - } -} - -#[cfg(test)] -mod test { - use super::*; - use graphql_parser::query; - use graphql_parser::Pos; - - fn mock_field(directives: Vec) -> schema::Field { - schema::Field { - position: Pos::default(), - description: None, - name: "foo".to_string(), - arguments: vec![], - field_type: schema::Type::NamedType("x".to_string()), - directives, - } - } - - #[test] - fn deprecation_no_reason() { - let directive = schema::Directive { - position: Pos::default(), - name: "deprecated".to_string(), - arguments: vec![], - }; - let result = parse_deprecation_info(&mock_field(vec![directive])); - assert_eq!(DeprecationStatus::Deprecated(None), result); - } - - #[test] - fn deprecation_with_reason() { - let directive = schema::Directive { - position: Pos::default(), - name: "deprecated".to_string(), - arguments: vec![( - "reason".to_string(), - query::Value::String("whatever".to_string()), - )], - }; - let result = parse_deprecation_info(&mock_field(vec![directive])); - assert_eq!( - DeprecationStatus::Deprecated(Some("whatever".to_string())), - result - ); - } - - #[test] - fn null_deprecation_reason() { - let directive = schema::Directive { - position: Pos::default(), - name: "deprecated".to_string(), - arguments: vec![("reason".to_string(), query::Value::Null)], - }; - let result = parse_deprecation_info(&mock_field(vec![directive])); - assert_eq!(DeprecationStatus::Deprecated(None), result); - } - - #[test] - #[should_panic] - fn invalid_deprecation_reason() { - let directive = schema::Directive { - position: Pos::default(), - name: "deprecated".to_string(), - arguments: vec![("reason".to_string(), query::Value::Boolean(true))], - }; - let _ = parse_deprecation_info(&mock_field(vec![directive])); - } - - #[test] - fn no_deprecation() { - let result = parse_deprecation_info(&mock_field(vec![])); - assert_eq!(DeprecationStatus::Current, result); - } -} diff --git a/graphql_client_codegen/src/operations.rs b/graphql_client_codegen/src/operations.rs deleted file mode 100644 index 5e42df2c6..000000000 --- a/graphql_client_codegen/src/operations.rs +++ /dev/null @@ -1,108 +0,0 @@ -use crate::constants::*; -use crate::query::QueryContext; -use crate::selection::Selection; -use crate::variables::Variable; -use graphql_parser::query::OperationDefinition; -use heck::SnakeCase; -use proc_macro2::{Span, TokenStream}; -use quote::quote; -use syn::Ident; - -#[derive(Debug, Clone)] -pub enum OperationType { - Query, - Mutation, - Subscription, -} - -#[derive(Debug, Clone)] -pub struct Operation<'query> { - pub name: String, - pub operation_type: OperationType, - pub variables: Vec>, - pub selection: Selection<'query>, -} - -impl<'query> Operation<'query> { - pub(crate) fn root_name<'schema>( - &self, - schema: &'schema crate::schema::Schema<'_>, - ) -> &'schema str { - match self.operation_type { - OperationType::Query => schema.query_type.unwrap_or("Query"), - OperationType::Mutation => schema.mutation_type.unwrap_or("Mutation"), - OperationType::Subscription => schema.subscription_type.unwrap_or("Subscription"), - } - } - - pub(crate) fn is_subscription(&self) -> bool { - match self.operation_type { - OperationType::Subscription => true, - _ => false, - } - } - - /// Generate the Variables struct and all the necessary supporting code. - pub(crate) fn expand_variables(&self, context: &QueryContext<'_, '_>) -> TokenStream { - let variables = &self.variables; - let variables_derives = context.variables_derives(); - - if variables.is_empty() { - return quote! { - #variables_derives - pub struct Variables; - }; - } - - let fields = variables.iter().map(|variable| { - let name = &variable.name; - let ty = variable.ty.to_rust(context, ""); - let snake_case_name = name.to_snake_case(); - let rename = crate::shared::field_rename_annotation(&name, &snake_case_name); - let name = Ident::new(&snake_case_name, Span::call_site()); - - quote!(#rename pub #name: #ty) - }); - - let default_constructors = variables - .iter() - .map(|variable| variable.generate_default_value_constructor(context)); - - quote! { - #variables_derives - pub struct Variables { - #(#fields,)* - } - - impl Variables { - #(#default_constructors)* - } - } - } -} - -impl<'query> std::convert::From<&'query OperationDefinition> for Operation<'query> { - fn from(definition: &'query OperationDefinition) -> Operation<'query> { - match *definition { - OperationDefinition::Query(ref q) => Operation { - name: q.name.clone().expect("unnamed operation"), - operation_type: OperationType::Query, - variables: q.variable_definitions.iter().map(|v| v.into()).collect(), - selection: (&q.selection_set).into(), - }, - OperationDefinition::Mutation(ref m) => Operation { - name: m.name.clone().expect("unnamed operation"), - operation_type: OperationType::Mutation, - variables: m.variable_definitions.iter().map(|v| v.into()).collect(), - selection: (&m.selection_set).into(), - }, - OperationDefinition::Subscription(ref s) => Operation { - name: s.name.clone().expect("unnamed operation"), - operation_type: OperationType::Subscription, - variables: s.variable_definitions.iter().map(|v| v.into()).collect(), - selection: (&s.selection_set).into(), - }, - OperationDefinition::SelectionSet(_) => panic!(SELECTION_SET_AT_ROOT), - } - } -} diff --git a/graphql_client_codegen/src/query.rs b/graphql_client_codegen/src/query.rs index c17443a76..71d0798fd 100644 --- a/graphql_client_codegen/src/query.rs +++ b/graphql_client_codegen/src/query.rs @@ -1,202 +1,753 @@ -use crate::deprecation::DeprecationStrategy; -use crate::fragments::GqlFragment; -use crate::schema::Schema; -use crate::selection::Selection; -use failure::*; -use proc_macro2::Span; -use proc_macro2::TokenStream; -use quote::quote; -use std::collections::{BTreeMap, BTreeSet}; -use syn::Ident; - -/// This holds all the information we need during the code generation phase. -pub(crate) struct QueryContext<'query, 'schema: 'query> { - pub fragments: BTreeMap<&'query str, GqlFragment<'query>>, - pub schema: &'schema Schema<'schema>, - pub deprecation_strategy: DeprecationStrategy, - variables_derives: Vec, - response_derives: Vec, -} - -impl<'query, 'schema> QueryContext<'query, 'schema> { - /// Create a QueryContext with the given Schema. - pub(crate) fn new( - schema: &'schema Schema<'schema>, - deprecation_strategy: DeprecationStrategy, - ) -> QueryContext<'query, 'schema> { - QueryContext { - fragments: BTreeMap::new(), - schema, - deprecation_strategy, - variables_derives: vec![Ident::new("Serialize", Span::call_site())], - response_derives: vec![Ident::new("Deserialize", Span::call_site())], +//! The responsibility of this module is to bind and validate a query +//! against a given schema. + +mod fragments; +mod operations; +mod selection; +mod validation; + +pub(crate) use fragments::{fragment_is_recursive, ResolvedFragment}; +pub(crate) use operations::ResolvedOperation; +pub(crate) use selection::*; + +use crate::{ + constants::TYPENAME_FIELD, + normalization::Normalization, + schema::{ + resolve_field_type, EnumId, InputId, ScalarId, Schema, StoredEnum, StoredFieldType, + StoredInputType, StoredScalar, TypeId, UnionId, + }, +}; +use std::{ + collections::{BTreeMap, BTreeSet}, + fmt::Display, +}; + +#[derive(Debug)] +pub(crate) struct QueryValidationError { + message: String, +} + +impl Display for QueryValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.message) + } +} + +impl std::error::Error for QueryValidationError {} + +impl QueryValidationError { + pub(crate) fn new(message: String) -> Self { + QueryValidationError { message } + } +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct SelectionId(u32); +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct OperationId(u32); + +impl OperationId { + pub(crate) fn new(idx: usize) -> Self { + OperationId(idx as u32) + } +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) struct ResolvedFragmentId(u32); + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy)] +pub(crate) struct VariableId(pub u32); + +pub(crate) fn resolve<'doc, T>( + schema: &Schema, + query: &graphql_parser::query::Document<'doc, T>, +) -> Result +where + T: graphql_parser::query::Text<'doc>, +{ + let mut resolved_query: Query = Default::default(); + + create_roots(&mut resolved_query, query, schema)?; + + // Then resolve the selections. + for definition in &query.definitions { + match definition { + graphql_parser::query::Definition::Fragment(fragment) => { + resolve_fragment(&mut resolved_query, schema, fragment)? + } + graphql_parser::query::Definition::Operation(operation) => { + resolve_operation(&mut resolved_query, schema, operation)? + } } } - /// Mark a fragment as required, so code is actually generated for it. - pub(crate) fn require_fragment(&self, typename_: &str) { - if let Some(fragment) = self.fragments.get(typename_) { - fragment.is_required.set(true) + // Validation: to be expanded and factored out. + validation::validate_typename_presence(&BoundQuery { + query: &resolved_query, + schema, + })?; + + for (selection_id, _) in resolved_query.selections() { + selection::validate_type_conditions( + selection_id, + &BoundQuery { + query: &resolved_query, + schema, + }, + )? + } + + Ok(resolved_query) +} + +fn create_roots<'doc, T>( + resolved_query: &mut Query, + query: &graphql_parser::query::Document<'doc, T>, + schema: &Schema, +) -> Result<(), QueryValidationError> +where + T: graphql_parser::query::Text<'doc>, +{ + // First, give ids to all fragments and operations. + for definition in &query.definitions { + match definition { + graphql_parser::query::Definition::Fragment(fragment) => { + let graphql_parser::query::TypeCondition::On(on) = &fragment.type_condition; + resolved_query.fragments.push(ResolvedFragment { + name: fragment.name.as_ref().into(), + on: schema.find_type(on.as_ref()).ok_or_else(|| { + QueryValidationError::new(format!( + "Could not find type {} for fragment {} in schema.", + on.as_ref(), + fragment.name.as_ref(), + )) + })?, + selection_set: Vec::new(), + }); + } + graphql_parser::query::Definition::Operation( + graphql_parser::query::OperationDefinition::Mutation(m), + ) => { + let on = schema.mutation_type().ok_or_else(|| { + QueryValidationError::new( + "Query contains a mutation operation, but the schema has no mutation type." + .to_owned(), + ) + })?; + let resolved_operation: ResolvedOperation = ResolvedOperation { + object_id: on, + name: m + .name + .as_ref() + .expect("mutation without name") + .as_ref() + .into(), + _operation_type: operations::OperationType::Mutation, + selection_set: Vec::with_capacity(m.selection_set.items.len()), + }; + + resolved_query.operations.push(resolved_operation); + } + graphql_parser::query::Definition::Operation( + graphql_parser::query::OperationDefinition::Query(q), + ) => { + let on = schema.query_type(); + let resolved_operation: ResolvedOperation = ResolvedOperation { + name: q.name.as_ref().expect("query without name. Instead of `query (...)`, write `query SomeName(...)` in your .graphql file").as_ref().into(), + _operation_type: operations::OperationType::Query, + object_id: on, + selection_set: Vec::with_capacity(q.selection_set.items.len()), + }; + + resolved_query.operations.push(resolved_operation); + } + graphql_parser::query::Definition::Operation( + graphql_parser::query::OperationDefinition::Subscription(s), + ) => { + let on = schema.subscription_type().ok_or_else(|| { + QueryValidationError::new( + "Query contains a subscription operation, but the schema has no subscription type.".to_owned() + ) + })?; + + if s.selection_set.items.len() != 1 { + return Err(QueryValidationError::new( + crate::constants::MULTIPLE_SUBSCRIPTION_FIELDS_ERROR.to_owned(), + )); + } + + let resolved_operation: ResolvedOperation = ResolvedOperation { + name: s + .name + .as_ref() + .expect("subscription without name") + .as_ref() + .into(), + _operation_type: operations::OperationType::Subscription, + object_id: on, + selection_set: Vec::with_capacity(s.selection_set.items.len()), + }; + + resolved_query.operations.push(resolved_operation); + } + graphql_parser::query::Definition::Operation( + graphql_parser::query::OperationDefinition::SelectionSet(_), + ) => { + return Err(QueryValidationError::new( + crate::constants::SELECTION_SET_AT_ROOT.to_owned(), + )) + } } } - /// For testing only. creates an empty QueryContext with an empty Schema. - #[cfg(test)] - pub(crate) fn new_empty(schema: &'schema Schema<'_>) -> QueryContext<'query, 'schema> { - QueryContext { - fragments: BTreeMap::new(), - schema, - deprecation_strategy: DeprecationStrategy::Allow, - variables_derives: vec![Ident::new("Serialize", Span::call_site())], - response_derives: vec![Ident::new("Deserialize", Span::call_site())], + Ok(()) +} + +fn resolve_fragment<'doc, T>( + query: &mut Query, + schema: &Schema, + fragment_definition: &graphql_parser::query::FragmentDefinition<'doc, T>, +) -> Result<(), QueryValidationError> +where + T: graphql_parser::query::Text<'doc>, +{ + let graphql_parser::query::TypeCondition::On(on) = &fragment_definition.type_condition; + let on = schema.find_type(on.as_ref()).ok_or_else(|| { + QueryValidationError::new(format!( + "Could not find type `{}` referenced by fragment `{}`", + on.as_ref(), + fragment_definition.name.as_ref(), + )) + })?; + + let (id, _) = query + .find_fragment(fragment_definition.name.as_ref()) + .ok_or_else(|| { + QueryValidationError::new(format!( + "Could not find fragment `{}`.", + fragment_definition.name.as_ref() + )) + })?; + + resolve_selection( + query, + on, + &fragment_definition.selection_set, + SelectionParent::Fragment(id), + schema, + )?; + + Ok(()) +} + +fn resolve_union_selection<'doc, T>( + query: &mut Query, + _union_id: UnionId, + selection_set: &graphql_parser::query::SelectionSet<'doc, T>, + parent: SelectionParent, + schema: &Schema, +) -> Result<(), QueryValidationError> +where + T: graphql_parser::query::Text<'doc>, +{ + for item in selection_set.items.iter() { + match item { + graphql_parser::query::Selection::Field(field) => { + if field.name.as_ref() == TYPENAME_FIELD { + let id = query.push_selection(Selection::Typename, parent); + parent.add_to_selection_set(query, id); + } else { + return Err(QueryValidationError::new(format!( + "Invalid field selection on union field ({:?})", + parent + ))); + } + } + graphql_parser::query::Selection::InlineFragment(inline_fragment) => { + let selection_id = resolve_inline_fragment(query, schema, inline_fragment, parent)?; + parent.add_to_selection_set(query, selection_id); + } + graphql_parser::query::Selection::FragmentSpread(fragment_spread) => { + let (fragment_id, _fragment) = query + .find_fragment(fragment_spread.fragment_name.as_ref()) + .ok_or_else(|| { + QueryValidationError::new(format!( + "Could not find fragment `{}` referenced by fragment spread.", + fragment_spread.fragment_name.as_ref() + )) + })?; + + let id = query.push_selection(Selection::FragmentSpread(fragment_id), parent); + + parent.add_to_selection_set(query, id); + } } } - /// Expand the deserialization data structures for the given field. - pub(crate) fn maybe_expand_field( - &self, - ty: &str, - selection: &Selection<'_>, - prefix: &str, - ) -> Result, failure::Error> { - if self.schema.contains_scalar(ty) { - Ok(None) - } else if let Some(enm) = self.schema.enums.get(ty) { - enm.is_required.set(true); - Ok(None) // we already expand enums separately - } else if let Some(obj) = self.schema.objects.get(ty) { - obj.is_required.set(true); - obj.response_for_selection(self, &selection, prefix) - .map(Some) - } else if let Some(iface) = self.schema.interfaces.get(ty) { - iface.is_required.set(true); - iface - .response_for_selection(self, &selection, prefix) - .map(Some) - } else if let Some(unn) = self.schema.unions.get(ty) { - unn.is_required.set(true); - unn.response_for_selection(self, &selection, prefix) - .map(Some) - } else { - Err(format_err!("Unknown type: {}", ty)) + Ok(()) +} + +fn resolve_object_selection<'a, 'doc, T>( + query: &mut Query, + object: &dyn crate::schema::ObjectLike, + selection_set: &graphql_parser::query::SelectionSet<'doc, T>, + parent: SelectionParent, + schema: &'a Schema, +) -> Result<(), QueryValidationError> +where + T: graphql_parser::query::Text<'doc>, +{ + for item in selection_set.items.iter() { + match item { + graphql_parser::query::Selection::Field(field) => { + if field.name.as_ref() == TYPENAME_FIELD { + let id = query.push_selection(Selection::Typename, parent); + parent.add_to_selection_set(query, id); + continue; + } + + let (field_id, schema_field) = object + .get_field_by_name(field.name.as_ref(), schema) + .ok_or_else(|| { + QueryValidationError::new(format!( + "No field named {} on {}", + field.name.as_ref(), + object.name() + )) + })?; + + let id = query.push_selection( + Selection::Field(SelectedField { + alias: field.alias.as_ref().map(|alias| alias.as_ref().into()), + field_id, + selection_set: Vec::with_capacity(selection_set.items.len()), + }), + parent, + ); + + resolve_selection( + query, + schema_field.r#type.id, + &field.selection_set, + SelectionParent::Field(id), + schema, + )?; + + parent.add_to_selection_set(query, id); + } + graphql_parser::query::Selection::InlineFragment(inline) => { + let selection_id = resolve_inline_fragment(query, schema, inline, parent)?; + + parent.add_to_selection_set(query, selection_id); + } + graphql_parser::query::Selection::FragmentSpread(fragment_spread) => { + let (fragment_id, _fragment) = query + .find_fragment(fragment_spread.fragment_name.as_ref()) + .ok_or_else(|| { + QueryValidationError::new(format!( + "Could not find fragment `{}` referenced by fragment spread.", + fragment_spread.fragment_name.as_ref() + )) + })?; + + let id = query.push_selection(Selection::FragmentSpread(fragment_id), parent); + + parent.add_to_selection_set(query, id); + } } } - pub(crate) fn ingest_additional_derives( - &mut self, - attribute_value: &str, - ) -> Result<(), failure::Error> { - if self.response_derives.len() > 1 { - return Err(format_err!( - "ingest_additional_derives should only be called once" - )); + Ok(()) +} + +fn resolve_selection<'doc, T>( + ctx: &mut Query, + on: TypeId, + selection_set: &graphql_parser::query::SelectionSet<'doc, T>, + parent: SelectionParent, + schema: &Schema, +) -> Result<(), QueryValidationError> +where + T: graphql_parser::query::Text<'doc>, +{ + match on { + TypeId::Object(oid) => { + let object = schema.get_object(oid); + resolve_object_selection(ctx, object, selection_set, parent, schema)?; + } + TypeId::Interface(interface_id) => { + let interface = schema.get_interface(interface_id); + resolve_object_selection(ctx, interface, selection_set, parent, schema)?; } + TypeId::Union(union_id) => { + resolve_union_selection(ctx, union_id, selection_set, parent, schema)?; + } + other => { + if !selection_set.items.is_empty() { + return Err(QueryValidationError::new(format!( + "Selection set on non-object, non-interface type. ({:?})", + other + ))); + } + } + }; + + Ok(()) +} + +fn resolve_inline_fragment<'doc, T>( + query: &mut Query, + schema: &Schema, + inline_fragment: &graphql_parser::query::InlineFragment<'doc, T>, + parent: SelectionParent, +) -> Result +where + T: graphql_parser::query::Text<'doc>, +{ + let graphql_parser::query::TypeCondition::On(on) = inline_fragment + .type_condition + .as_ref() + .expect("missing type condition on inline fragment"); + let type_id = schema.find_type(on.as_ref()).ok_or_else(|| { + QueryValidationError::new(format!( + "Could not find type `{}` referenced by inline fragment.", + on.as_ref() + )) + })?; + + let id = query.push_selection( + Selection::InlineFragment(InlineFragment { + type_id, + selection_set: Vec::with_capacity(inline_fragment.selection_set.items.len()), + }), + parent, + ); + + resolve_selection( + query, + type_id, + &inline_fragment.selection_set, + SelectionParent::InlineFragment(id), + schema, + )?; + + Ok(id) +} + +fn resolve_operation<'doc, T>( + query: &mut Query, + schema: &Schema, + operation: &graphql_parser::query::OperationDefinition<'doc, T>, +) -> Result<(), QueryValidationError> +where + T: graphql_parser::query::Text<'doc>, +{ + match operation { + graphql_parser::query::OperationDefinition::Mutation(m) => { + let on = schema.mutation_type().ok_or_else(|| { + QueryValidationError::new( + "Query contains a mutation operation, but the schema has no mutation type." + .to_owned(), + ) + })?; + let on = schema.get_object(on); + + let (id, _) = query + .find_operation(m.name.as_ref().map(|name| name.as_ref()).unwrap()) + .unwrap(); - self.variables_derives.extend( - attribute_value - .split(',') - .map(str::trim) - .map(|s| Ident::new(s, Span::call_site())), - ); - self.response_derives.extend( - attribute_value - .split(',') - .map(str::trim) - .map(|s| Ident::new(s, Span::call_site())), - ); - Ok(()) - } - - pub(crate) fn variables_derives(&self) -> TokenStream { - let derives: BTreeSet<&Ident> = self.variables_derives.iter().collect(); - let derives = derives.iter(); - - quote! { - #[derive( #(#derives),* )] + resolve_variables(query, &m.variable_definitions, schema, id); + resolve_object_selection( + query, + on, + &m.selection_set, + SelectionParent::Operation(id), + schema, + )?; } - } + graphql_parser::query::OperationDefinition::Query(q) => { + let on = schema.get_object(schema.query_type()); + let (id, _) = query + .find_operation(q.name.as_ref().map(|name| name.as_ref()).unwrap()) + .unwrap(); - pub(crate) fn response_derives(&self) -> TokenStream { - let derives: BTreeSet<&Ident> = self.response_derives.iter().collect(); - let derives = derives.iter(); + resolve_variables(query, &q.variable_definitions, schema, id); + resolve_object_selection( + query, + on, + &q.selection_set, + SelectionParent::Operation(id), + schema, + )?; + } + graphql_parser::query::OperationDefinition::Subscription(s) => { + let on = schema.subscription_type().ok_or_else(|| QueryValidationError::new("Query contains a subscription operation, but the schema has no subscription type.".into()))?; + let on = schema.get_object(on); + let (id, _) = query + .find_operation(s.name.as_ref().map(|name| name.as_ref()).unwrap()) + .unwrap(); - quote! { - #[derive( #(#derives),* )] + resolve_variables(query, &s.variable_definitions, schema, id); + resolve_object_selection( + query, + on, + &s.selection_set, + SelectionParent::Operation(id), + schema, + )?; } + graphql_parser::query::OperationDefinition::SelectionSet(_) => { + unreachable!("unnamed queries are not supported") + } + } + + Ok(()) +} + +#[derive(Default)] +pub(crate) struct Query { + fragments: Vec, + operations: Vec, + selection_parent_idx: BTreeMap, + selections: Vec, + pub(crate) variables: Vec, +} + +impl Query { + fn push_selection(&mut self, node: Selection, parent: SelectionParent) -> SelectionId { + let id = SelectionId(self.selections.len() as u32); + self.selections.push(node); + + self.selection_parent_idx.insert(id, parent); + + id + } + + pub fn operations(&self) -> impl Iterator { + walk_operations(self) + } + + pub(crate) fn get_selection(&self, id: SelectionId) -> &Selection { + self.selections + .get(id.0 as usize) + .expect("Query.get_selection") } - pub(crate) fn response_enum_derives(&self) -> TokenStream { - let always_derives = [ - Ident::new("Eq", Span::call_site()), - Ident::new("PartialEq", Span::call_site()), - ]; - let mut enum_derives: BTreeSet<_> = self - .response_derives + pub(crate) fn get_fragment(&self, id: ResolvedFragmentId) -> &ResolvedFragment { + self.fragments + .get(id.0 as usize) + .expect("Query.get_fragment") + } + + pub(crate) fn get_operation(&self, id: OperationId) -> &ResolvedOperation { + self.operations + .get(id.0 as usize) + .expect("Query.get_operation") + } + + /// Selects the first operation matching `struct_name`. Returns `None` when the query document defines no operation, or when the selected operation does not match any defined operation. + pub(crate) fn select_operation<'a>( + &'a self, + name: &str, + normalization: Normalization, + ) -> Option<(OperationId, &'a ResolvedOperation)> { + walk_operations(self).find(|(_id, op)| normalization.operation(&op.name) == name) + } + + fn find_fragment(&mut self, name: &str) -> Option<(ResolvedFragmentId, &mut ResolvedFragment)> { + self.fragments + .iter_mut() + .enumerate() + .find(|(_, frag)| frag.name == name) + .map(|(id, f)| (ResolvedFragmentId(id as u32), f)) + } + + fn find_operation(&mut self, name: &str) -> Option<(OperationId, &mut ResolvedOperation)> { + self.operations + .iter_mut() + .enumerate() + .find(|(_, op)| op.name == name) + .map(|(id, op)| (OperationId::new(id), op)) + } + + fn selections(&self) -> impl Iterator { + self.selections + .iter() + .enumerate() + .map(|(idx, selection)| (SelectionId(idx as u32), selection)) + } + + fn walk_selection_set<'a>( + &'a self, + selection_ids: &'a [SelectionId], + ) -> impl Iterator + 'a { + selection_ids .iter() - .filter(|derive| { - !derive.to_string().contains("erialize") - && !derive.to_string().contains("Deserialize") - }) - .collect(); - enum_derives.extend(always_derives.iter()); - quote! { - #[derive( #(#enum_derives),* )] + .map(move |id| (*id, self.get_selection(*id))) + } +} + +#[derive(Debug)] +pub(crate) struct ResolvedVariable { + pub(crate) operation_id: OperationId, + pub(crate) name: String, + pub(crate) default: Option>, + pub(crate) r#type: StoredFieldType, +} + +impl ResolvedVariable { + pub(crate) fn type_name<'schema>(&self, schema: &'schema Schema) -> &'schema str { + self.r#type.id.name(schema) + } + + fn collect_used_types(&self, used_types: &mut UsedTypes, schema: &Schema) { + match self.r#type.id { + TypeId::Input(input_id) => { + used_types.types.insert(TypeId::Input(input_id)); + + let input = schema.get_input(input_id); + + input.used_input_ids_recursive(used_types, schema) + } + type_id @ TypeId::Scalar(_) | type_id @ TypeId::Enum(_) => { + used_types.types.insert(type_id); + } + _ => (), } } } -#[cfg(test)] -mod tests { - use super::*; +#[derive(Debug, Default)] +pub(crate) struct UsedTypes { + pub(crate) types: BTreeSet, + fragments: BTreeSet, +} - #[test] - fn response_derives_ingestion_works() { - let schema = crate::schema::Schema::new(); - let mut context = QueryContext::new_empty(&schema); +impl UsedTypes { + pub(crate) fn inputs<'s, 'a: 's>( + &'s self, + schema: &'a Schema, + ) -> impl Iterator + 's { + schema + .inputs() + .filter(move |(id, _input)| self.types.contains(&TypeId::Input(*id))) + } - context - .ingest_additional_derives("PartialEq, PartialOrd, Serialize") - .unwrap(); + pub(crate) fn scalars<'s, 'a: 's>( + &'s self, + schema: &'a Schema, + ) -> impl Iterator + 's { + self.types + .iter() + .filter_map(TypeId::as_scalar_id) + .map(move |scalar_id| (scalar_id, schema.get_scalar(scalar_id))) + .filter(|(_id, scalar)| !crate::schema::DEFAULT_SCALARS.contains(&scalar.name.as_str())) + } - assert_eq!( - context.response_derives().to_string(), - "# [ derive ( Deserialize , PartialEq , PartialOrd , Serialize ) ]" - ); + pub(crate) fn enums<'a, 'schema: 'a>( + &'a self, + schema: &'schema Schema, + ) -> impl Iterator + 'a { + self.types + .iter() + .filter_map(TypeId::as_enum_id) + .map(move |enum_id| (enum_id, schema.get_enum(enum_id))) } - #[test] - fn response_enum_derives_does_not_produce_empty_list() { - let schema = crate::schema::Schema::new(); - let context = QueryContext::new_empty(&schema); - assert_eq!( - context.response_enum_derives().to_string(), - "# [ derive ( Eq , PartialEq ) ]" - ); + pub(crate) fn fragment_ids(&self) -> impl Iterator + '_ { + self.fragments.iter().copied() } +} - #[test] - fn response_enum_derives_works() { - let schema = crate::schema::Schema::new(); - let mut context = QueryContext::new_empty(&schema); +fn resolve_variables<'doc, T>( + query: &mut Query, + variables: &[graphql_parser::query::VariableDefinition<'doc, T>], + schema: &Schema, + operation_id: OperationId, +) where + T: graphql_parser::query::Text<'doc>, +{ + for var in variables { + query.variables.push(ResolvedVariable { + operation_id, + name: var.name.as_ref().into(), + default: var.default_value.as_ref().map(|dflt| dflt.into_static()), + r#type: resolve_field_type(schema, &var.var_type), + }); + } +} - context - .ingest_additional_derives("PartialEq, PartialOrd, Serialize") - .unwrap(); +pub(crate) fn walk_operations( + query: &Query, +) -> impl Iterator { + query + .operations + .iter() + .enumerate() + .map(|(id, op)| (OperationId(id as u32), op)) +} - assert_eq!( - context.response_enum_derives().to_string(), - "# [ derive ( Eq , PartialEq , PartialOrd ) ]" - ); +pub(crate) fn operation_has_no_variables(operation_id: OperationId, query: &Query) -> bool { + walk_operation_variables(operation_id, query) + .next() + .is_none() +} + +pub(crate) fn walk_operation_variables( + operation_id: OperationId, + query: &Query, +) -> impl Iterator { + query + .variables + .iter() + .enumerate() + .map(|(idx, var)| (VariableId(idx as u32), var)) + .filter(move |(_id, var)| var.operation_id == operation_id) +} + +pub(crate) fn all_used_types(operation_id: OperationId, query: &BoundQuery<'_>) -> UsedTypes { + let mut used_types = UsedTypes::default(); + + let operation = query.query.get_operation(operation_id); + + for (_id, selection) in query.query.walk_selection_set(&operation.selection_set) { + selection.collect_used_types(&mut used_types, query); + } + + for (_id, variable) in walk_operation_variables(operation_id, query.query) { + variable.collect_used_types(&mut used_types, query.schema); } - #[test] - fn response_derives_fails_when_called_twice() { - let schema = crate::schema::Schema::new(); - let mut context = QueryContext::new_empty(&schema); + used_types +} - assert!(context - .ingest_additional_derives("PartialEq, PartialOrd") - .is_ok()); - assert!(context.ingest_additional_derives("Serialize").is_err()); +pub(crate) fn full_path_prefix(selection_id: SelectionId, query: &BoundQuery<'_>) -> String { + let mut path = match query.query.get_selection(selection_id) { + Selection::FragmentSpread(_) | Selection::InlineFragment(_) => Vec::new(), + selection => vec![selection.to_path_segment(query)], + }; + + let mut item = selection_id; + + while let Some(parent) = query.query.selection_parent_idx.get(&item) { + path.push(parent.to_path_segment(query)); + + match parent { + SelectionParent::Field(id) | SelectionParent::InlineFragment(id) => { + item = *id; + } + _ => break, + } } + + path.reverse(); + path.join("") +} + +#[derive(Clone, Copy)] +pub(crate) struct BoundQuery<'a> { + pub(crate) query: &'a Query, + pub(crate) schema: &'a Schema, } diff --git a/graphql_client_codegen/src/query/fragments.rs b/graphql_client_codegen/src/query/fragments.rs new file mode 100644 index 000000000..9f5c73b2b --- /dev/null +++ b/graphql_client_codegen/src/query/fragments.rs @@ -0,0 +1,24 @@ +use super::{Query, ResolvedFragmentId, SelectionId}; +use crate::schema::TypeId; +use heck::*; + +#[derive(Debug)] +pub(crate) struct ResolvedFragment { + pub(crate) name: String, + pub(crate) on: TypeId, + pub(crate) selection_set: Vec, +} + +impl ResolvedFragment { + pub(super) fn to_path_segment(&self) -> String { + self.name.to_upper_camel_case() + } +} + +pub(crate) fn fragment_is_recursive(fragment_id: ResolvedFragmentId, query: &Query) -> bool { + let fragment = query.get_fragment(fragment_id); + + query + .walk_selection_set(&fragment.selection_set) + .any(|(_id, selection)| selection.contains_fragment(fragment_id, query)) +} diff --git a/graphql_client_codegen/src/query/operations.rs b/graphql_client_codegen/src/query/operations.rs new file mode 100644 index 000000000..fe7149ac1 --- /dev/null +++ b/graphql_client_codegen/src/query/operations.rs @@ -0,0 +1,23 @@ +use super::SelectionId; +use crate::schema::ObjectId; +use heck::*; + +#[derive(Debug, Clone)] +pub(crate) enum OperationType { + Query, + Mutation, + Subscription, +} + +pub(crate) struct ResolvedOperation { + pub(crate) name: String, + pub(crate) _operation_type: OperationType, + pub(crate) selection_set: Vec, + pub(crate) object_id: ObjectId, +} + +impl ResolvedOperation { + pub(crate) fn to_path_segment(&self) -> String { + self.name.to_upper_camel_case() + } +} diff --git a/graphql_client_codegen/src/query/selection.rs b/graphql_client_codegen/src/query/selection.rs new file mode 100644 index 000000000..018e6f22a --- /dev/null +++ b/graphql_client_codegen/src/query/selection.rs @@ -0,0 +1,270 @@ +use super::{ + BoundQuery, OperationId, Query, QueryValidationError, ResolvedFragmentId, SelectionId, + UsedTypes, +}; +use crate::schema::{Schema, StoredField, StoredFieldId, TypeId}; +use heck::ToUpperCamelCase; + +/// This checks that the `on` clause on fragment spreads and inline fragments +/// are valid in their context. +pub(super) fn validate_type_conditions( + selection_id: SelectionId, + query: &BoundQuery<'_>, +) -> Result<(), QueryValidationError> { + let selection = query.query.get_selection(selection_id); + + let selected_type = match selection { + Selection::FragmentSpread(fragment_id) => query.query.get_fragment(*fragment_id).on, + Selection::InlineFragment(inline_fragment) => inline_fragment.type_id, + _ => return Ok(()), + }; + + let parent_schema_type_id = query + .query + .selection_parent_idx + .get(&selection_id) + .expect("Could not find selection parent") + .schema_type_id(query); + + if parent_schema_type_id == selected_type { + return Ok(()); + } + + match parent_schema_type_id { + TypeId::Union(union_id) => { + let union = query.schema.get_union(union_id); + + if !union + .variants + .iter() + .any(|variant| *variant == selected_type) + { + return Err(QueryValidationError::new(format!( + "The spread {}... on {} is not valid.", + union.name, + selected_type.name(query.schema) + ))); + } + } + TypeId::Interface(interface_id) => { + let mut variants = query + .schema + .objects() + .filter(|(_, obj)| obj.implements_interfaces.contains(&interface_id)); + + if !variants.any(|(id, _)| TypeId::Object(id) == selected_type) { + return Err(QueryValidationError::new(format!( + "The spread {}... on {} is not valid.", + parent_schema_type_id.name(query.schema), + selected_type.name(query.schema), + ))); + } + } + _ => (), + } + + Ok(()) +} + +#[derive(Debug, Clone, Copy)] +pub(super) enum SelectionParent { + Field(SelectionId), + InlineFragment(SelectionId), + Fragment(ResolvedFragmentId), + Operation(OperationId), +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +impl SelectionParent { + fn schema_type_id(&self, query: &BoundQuery<'_>) -> TypeId { + match self { + SelectionParent::Fragment(fragment_id) => query.query.get_fragment(*fragment_id).on, + SelectionParent::Operation(operation_id) => { + TypeId::Object(query.query.get_operation(*operation_id).object_id) + } + SelectionParent::Field(id) => { + let field_id = query + .query + .get_selection(*id) + .as_selected_field() + .unwrap() + .field_id; + query.schema.get_field(field_id).r#type.id + } + SelectionParent::InlineFragment(id) => { + { query.query.get_selection(*id).as_inline_fragment().unwrap() }.type_id + } + } + } + + pub(super) fn add_to_selection_set(&self, q: &mut Query, selection_id: SelectionId) { + match self { + SelectionParent::Field(parent_selection_id) + | SelectionParent::InlineFragment(parent_selection_id) => { + let parent_selection = q + .selections + .get_mut(parent_selection_id.0 as usize) + .expect("get parent selection"); + + match parent_selection { + Selection::Field(f) => f.selection_set.push(selection_id), + Selection::InlineFragment(inline) => inline.selection_set.push(selection_id), + other => unreachable!("impossible parent selection: {:?}", other), + } + } + SelectionParent::Fragment(fragment_id) => { + let fragment = q + .fragments + .get_mut(fragment_id.0 as usize) + .expect("get fragment"); + + fragment.selection_set.push(selection_id); + } + SelectionParent::Operation(operation_id) => { + let operation = q + .operations + .get_mut(operation_id.0 as usize) + .expect("get operation"); + + operation.selection_set.push(selection_id); + } + } + } + + pub(crate) fn to_path_segment(self, query: &BoundQuery<'_>) -> String { + match self { + SelectionParent::Field(id) | SelectionParent::InlineFragment(id) => { + query.query.get_selection(id).to_path_segment(query) + } + SelectionParent::Operation(id) => query.query.get_operation(id).to_path_segment(), + SelectionParent::Fragment(id) => query.query.get_fragment(id).to_path_segment(), + } + } +} + +#[derive(Debug)] +pub(crate) enum Selection { + Field(SelectedField), + InlineFragment(InlineFragment), + FragmentSpread(ResolvedFragmentId), + Typename, +} + +impl Selection { + pub(crate) fn as_selected_field(&self) -> Option<&SelectedField> { + match self { + Selection::Field(f) => Some(f), + _ => None, + } + } + + pub(crate) fn as_inline_fragment(&self) -> Option<&InlineFragment> { + match self { + Selection::InlineFragment(f) => Some(f), + _ => None, + } + } + + pub(crate) fn collect_used_types(&self, used_types: &mut UsedTypes, query: &BoundQuery<'_>) { + match self { + Selection::Field(field) => { + let stored_field = query.schema.get_field(field.field_id); + used_types.types.insert(stored_field.r#type.id); + + for selection_id in self.subselection() { + let selection = query.query.get_selection(*selection_id); + selection.collect_used_types(used_types, query); + } + } + Selection::InlineFragment(inline_fragment) => { + used_types.types.insert(inline_fragment.type_id); + + for selection_id in self.subselection() { + let selection = query.query.get_selection(*selection_id); + selection.collect_used_types(used_types, query); + } + } + Selection::FragmentSpread(fragment_id) => { + // This is necessary to avoid infinite recursion. + if used_types.fragments.contains(fragment_id) { + return; + } + + used_types.fragments.insert(*fragment_id); + + let fragment = query.query.get_fragment(*fragment_id); + + for (_id, selection) in query.query.walk_selection_set(&fragment.selection_set) { + selection.collect_used_types(used_types, query); + } + } + Selection::Typename => (), + } + } + + pub(crate) fn contains_fragment(&self, fragment_id: ResolvedFragmentId, query: &Query) -> bool { + match self { + Selection::FragmentSpread(id) => *id == fragment_id, + _ => self.subselection().iter().any(|selection_id| { + query + .get_selection(*selection_id) + .contains_fragment(fragment_id, query) + }), + } + } + + pub(crate) fn subselection(&self) -> &[SelectionId] { + match self { + Selection::Field(field) => field.selection_set.as_slice(), + Selection::InlineFragment(inline_fragment) => &inline_fragment.selection_set, + _ => &[], + } + } + + pub(super) fn to_path_segment(&self, query: &BoundQuery<'_>) -> String { + match self { + Selection::Field(field) => field + .alias + .as_ref() + .map(|alias| alias.to_upper_camel_case()) + .unwrap_or_else(move || { + query + .schema + .get_field(field.field_id) + .name + .to_upper_camel_case() + }), + Selection::InlineFragment(inline_fragment) => format!( + "On{}", + inline_fragment + .type_id + .name(query.schema) + .to_upper_camel_case() + ), + other => unreachable!("{:?} in to_path_segment", other), + } + } +} + +#[derive(Debug)] +pub(crate) struct InlineFragment { + pub(crate) type_id: TypeId, + pub(crate) selection_set: Vec, +} + +#[derive(Debug)] +pub(crate) struct SelectedField { + pub(crate) alias: Option, + pub(crate) field_id: StoredFieldId, + pub(crate) selection_set: Vec, +} + +impl SelectedField { + pub(crate) fn alias(&self) -> Option<&str> { + self.alias.as_deref() + } + + pub(crate) fn schema_field<'a>(&self, schema: &'a Schema) -> &'a StoredField { + schema.get_field(self.field_id) + } +} diff --git a/graphql_client_codegen/src/query/validation.rs b/graphql_client_codegen/src/query/validation.rs new file mode 100644 index 000000000..ee0a48f58 --- /dev/null +++ b/graphql_client_codegen/src/query/validation.rs @@ -0,0 +1,72 @@ +use super::{full_path_prefix, BoundQuery, Query, QueryValidationError, Selection, SelectionId}; +use crate::schema::TypeId; + +pub(super) fn validate_typename_presence( + query: &BoundQuery<'_>, +) -> Result<(), QueryValidationError> { + for fragment in query.query.fragments.iter() { + let type_id = match fragment.on { + id @ TypeId::Interface(_) | id @ TypeId::Union(_) => id, + _ => continue, + }; + + if !selection_set_contains_type_name(fragment.on, &fragment.selection_set, query.query) { + return Err(QueryValidationError::new(format!( + "The `{}` fragment uses `{}` but does not select `__typename` on it. graphql-client cannot generate code for it. Please add `__typename` to the selection.", + &fragment.name, + type_id.name(query.schema), + ))); + } + } + + let union_and_interface_field_selections = + query + .query + .selections() + .filter_map(|(selection_id, selection)| match selection { + Selection::Field(field) => match query.schema.get_field(field.field_id).r#type.id { + id @ TypeId::Interface(_) | id @ TypeId::Union(_) => { + Some((selection_id, id, &field.selection_set)) + } + _ => None, + }, + _ => None, + }); + + for selection in union_and_interface_field_selections { + if !selection_set_contains_type_name(selection.1, selection.2, query.query) { + return Err(QueryValidationError::new(format!( + "The query uses `{path}` at `{selected_type}` but does not select `__typename` on it. graphql-client cannot generate code for it. Please add `__typename` to the selection.", + path = full_path_prefix(selection.0, query), + selected_type = selection.1.name(query.schema) + ))); + } + } + + Ok(()) +} + +fn selection_set_contains_type_name( + parent_type_id: TypeId, + selection_set: &[SelectionId], + query: &Query, +) -> bool { + for id in selection_set { + let selection = query.get_selection(*id); + + match selection { + Selection::Typename => return true, + Selection::FragmentSpread(fragment_id) => { + let fragment = query.get_fragment(*fragment_id); + if fragment.on == parent_type_id + && selection_set_contains_type_name(fragment.on, &fragment.selection_set, query) + { + return true; + } + } + _ => (), + } + } + + false +} diff --git a/graphql_client_codegen/src/scalars.rs b/graphql_client_codegen/src/scalars.rs deleted file mode 100644 index a596dd343..000000000 --- a/graphql_client_codegen/src/scalars.rs +++ /dev/null @@ -1,19 +0,0 @@ -use quote::quote; -use std::cell::Cell; - -#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq)] -pub struct Scalar<'schema> { - pub name: &'schema str, - pub description: Option<&'schema str>, - pub is_required: Cell, -} - -impl<'schema> Scalar<'schema> { - // TODO: do something smarter here - pub fn to_rust(&self) -> proc_macro2::TokenStream { - use proc_macro2::{Ident, Span}; - let ident = Ident::new(&self.name, Span::call_site()); - let description = self.description.map(|d| quote!(#[doc = #d])); - quote!(#description type #ident = super::#ident;) - } -} diff --git a/graphql_client_codegen/src/schema.rs b/graphql_client_codegen/src/schema.rs index 9c472ea63..00bc77ca4 100644 --- a/graphql_client_codegen/src/schema.rs +++ b/graphql_client_codegen/src/schema.rs @@ -1,455 +1,538 @@ -use crate::deprecation::DeprecationStatus; -use crate::enums::{EnumVariant, GqlEnum}; -use crate::field_type::FieldType; -use crate::inputs::GqlInput; -use crate::interfaces::GqlInterface; -use crate::objects::{GqlObject, GqlObjectField}; -use crate::scalars::Scalar; -use crate::unions::GqlUnion; -use failure::*; -use graphql_parser::{self, schema}; +mod graphql_parser_conversion; +mod json_conversion; + +#[cfg(test)] +mod tests; + +use crate::query::UsedTypes; +use crate::type_qualifiers::GraphqlTypeQualifier; use std::collections::{BTreeMap, BTreeSet}; pub(crate) const DEFAULT_SCALARS: &[&str] = &["ID", "String", "Int", "Float", "Boolean"]; +#[derive(Debug, PartialEq, Clone)] +pub(crate) struct StoredObject { + pub(crate) name: String, + pub(crate) fields: Vec, + pub(crate) implements_interfaces: Vec, +} + +#[derive(Debug, PartialEq, Clone)] +pub(crate) struct StoredField { + pub(crate) name: String, + pub(crate) r#type: StoredFieldType, + pub(crate) parent: StoredFieldParent, + /// `Some(None)` should be interpreted as "deprecated, without reason" + pub(crate) deprecation: Option>, +} + +impl StoredField { + pub(crate) fn deprecation(&self) -> Option> { + self.deprecation.as_ref().map(|inner| inner.as_deref()) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub(crate) enum StoredFieldParent { + Object(ObjectId), + Interface(InterfaceId), +} + +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)] +pub(crate) struct ObjectId(u32); + +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)] +pub(crate) struct InterfaceId(usize); + +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)] +pub(crate) struct ScalarId(usize); + +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)] +pub(crate) struct UnionId(usize); + +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)] +pub(crate) struct EnumId(usize); + +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)] +pub(crate) struct InputId(u32); + +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct StoredFieldId(usize); + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct StoredInterface { + name: String, + fields: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct StoredFieldType { + pub(crate) id: TypeId, + /// An ordered list of qualifiers, from outer to inner. + /// + /// e.g. `[Int]!` would have `vec![List, Optional]`, but `[Int!]` would have `vec![Optional, + /// List]`. + pub(crate) qualifiers: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct StoredUnion { + pub(crate) name: String, + pub(crate) variants: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct StoredScalar { + pub(crate) name: String, +} + +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)] +pub(crate) enum TypeId { + Object(ObjectId), + Scalar(ScalarId), + Interface(InterfaceId), + Union(UnionId), + Enum(EnumId), + Input(InputId), +} + +impl TypeId { + fn r#enum(id: usize) -> Self { + TypeId::Enum(EnumId(id)) + } + + fn interface(id: usize) -> Self { + TypeId::Interface(InterfaceId(id)) + } + + fn union(id: usize) -> Self { + TypeId::Union(UnionId(id)) + } + + fn object(id: u32) -> Self { + TypeId::Object(ObjectId(id)) + } + + fn input(id: u32) -> Self { + TypeId::Input(InputId(id)) + } + + fn as_interface_id(&self) -> Option { + match self { + TypeId::Interface(id) => Some(*id), + _ => None, + } + } + + fn as_object_id(&self) -> Option { + match self { + TypeId::Object(id) => Some(*id), + _ => None, + } + } + + pub(crate) fn as_input_id(&self) -> Option { + match self { + TypeId::Input(id) => Some(*id), + _ => None, + } + } + + pub(crate) fn as_scalar_id(&self) -> Option { + match self { + TypeId::Scalar(id) => Some(*id), + _ => None, + } + } + + pub(crate) fn as_enum_id(&self) -> Option { + match self { + TypeId::Enum(id) => Some(*id), + _ => None, + } + } + + pub(crate) fn name<'a>(&self, schema: &'a Schema) -> &'a str { + match self { + TypeId::Object(obj) => schema.get_object(*obj).name.as_str(), + TypeId::Scalar(s) => schema.get_scalar(*s).name.as_str(), + TypeId::Interface(s) => schema.get_interface(*s).name.as_str(), + TypeId::Union(s) => schema.get_union(*s).name.as_str(), + TypeId::Enum(s) => schema.get_enum(*s).name.as_str(), + TypeId::Input(s) => schema.get_input(*s).name.as_str(), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct StoredEnum { + pub(crate) name: String, + pub(crate) variants: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct StoredInputFieldType { + pub(crate) id: TypeId, + pub(crate) qualifiers: Vec, +} + +impl StoredInputFieldType { + /// A type is indirected if it is a (flat or nested) list type, optional or not. + /// + /// We use this to determine whether a type needs to be boxed for recursion. + pub(crate) fn is_indirected(&self) -> bool { + self.qualifiers + .iter() + .any(|qualifier| qualifier == &GraphqlTypeQualifier::List) + } + + pub(crate) fn is_optional(&self) -> bool { + self.qualifiers + .first() + .map(|qualifier| !qualifier.is_required()) + .unwrap_or(true) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct StoredInputType { + pub(crate) name: String, + pub(crate) fields: Vec<(String, StoredInputFieldType)>, + pub(crate) is_one_of: bool, +} + /// Intermediate representation for a parsed GraphQL schema used during code generation. #[derive(Debug, Clone, PartialEq)] -pub(crate) struct Schema<'schema> { - pub(crate) enums: BTreeMap<&'schema str, GqlEnum<'schema>>, - pub(crate) inputs: BTreeMap<&'schema str, GqlInput<'schema>>, - pub(crate) interfaces: BTreeMap<&'schema str, GqlInterface<'schema>>, - pub(crate) objects: BTreeMap<&'schema str, GqlObject<'schema>>, - pub(crate) scalars: BTreeMap<&'schema str, Scalar<'schema>>, - pub(crate) unions: BTreeMap<&'schema str, GqlUnion<'schema>>, - pub(crate) query_type: Option<&'schema str>, - pub(crate) mutation_type: Option<&'schema str>, - pub(crate) subscription_type: Option<&'schema str>, +pub(crate) struct Schema { + stored_objects: Vec, + stored_fields: Vec, + stored_interfaces: Vec, + stored_unions: Vec, + stored_scalars: Vec, + stored_enums: Vec, + stored_inputs: Vec, + names: BTreeMap, + + pub(crate) query_type: Option, + pub(crate) mutation_type: Option, + pub(crate) subscription_type: Option, } -impl<'schema> Schema<'schema> { - pub(crate) fn new() -> Schema<'schema> { - Schema { - enums: BTreeMap::new(), - inputs: BTreeMap::new(), - interfaces: BTreeMap::new(), - objects: BTreeMap::new(), - scalars: BTreeMap::new(), - unions: BTreeMap::new(), +impl Schema { + pub(crate) fn new() -> Schema { + let mut schema = Schema { + stored_objects: Vec::new(), + stored_interfaces: Vec::new(), + stored_fields: Vec::new(), + stored_unions: Vec::new(), + stored_scalars: Vec::with_capacity(DEFAULT_SCALARS.len()), + stored_enums: Vec::new(), + stored_inputs: Vec::new(), + names: BTreeMap::new(), query_type: None, mutation_type: None, subscription_type: None, + }; + + schema.push_default_scalars(); + + schema + } + + fn push_default_scalars(&mut self) { + for scalar in DEFAULT_SCALARS { + let id = self.push_scalar(StoredScalar { + name: (*scalar).to_owned(), + }); + + self.names.insert((*scalar).to_owned(), TypeId::Scalar(id)); } } - pub(crate) fn ingest_interface_implementations( - &mut self, - impls: BTreeMap<&'schema str, Vec<&'schema str>>, - ) -> Result<(), failure::Error> { - impls - .into_iter() - .map(|(iface_name, implementors)| { - let iface = self - .interfaces - .get_mut(&iface_name) - .ok_or_else(|| format_err!("interface not found: {}", iface_name))?; - iface.implemented_by = implementors.iter().cloned().collect(); - Ok(()) - }) - .collect() - } - - pub(crate) fn require(&self, typename_: &str) { - DEFAULT_SCALARS + fn push_object(&mut self, object: StoredObject) -> ObjectId { + let id = ObjectId(self.stored_objects.len() as u32); + self.stored_objects.push(object); + + id + } + + fn push_interface(&mut self, interface: StoredInterface) -> InterfaceId { + let id = InterfaceId(self.stored_interfaces.len()); + + self.stored_interfaces.push(interface); + + id + } + + fn push_scalar(&mut self, scalar: StoredScalar) -> ScalarId { + let id = ScalarId(self.stored_scalars.len()); + + self.stored_scalars.push(scalar); + + id + } + + fn push_enum(&mut self, enm: StoredEnum) -> EnumId { + let id = EnumId(self.stored_enums.len()); + + self.stored_enums.push(enm); + + id + } + + fn push_field(&mut self, field: StoredField) -> StoredFieldId { + let id = StoredFieldId(self.stored_fields.len()); + + self.stored_fields.push(field); + + id + } + + pub(crate) fn query_type(&self) -> ObjectId { + self.query_type + .expect("Query operation type must be defined") + } + + pub(crate) fn mutation_type(&self) -> Option { + self.mutation_type + } + + pub(crate) fn subscription_type(&self) -> Option { + self.subscription_type + } + + pub(crate) fn get_interface(&self, interface_id: InterfaceId) -> &StoredInterface { + self.stored_interfaces.get(interface_id.0).unwrap() + } + + pub(crate) fn get_input(&self, input_id: InputId) -> &StoredInputType { + self.stored_inputs.get(input_id.0 as usize).unwrap() + } + + pub(crate) fn get_object(&self, object_id: ObjectId) -> &StoredObject { + self.stored_objects + .get(object_id.0 as usize) + .expect("Schema::get_object") + } + + pub(crate) fn get_object_mut(&mut self, object_id: ObjectId) -> &mut StoredObject { + self.stored_objects + .get_mut(object_id.0 as usize) + .expect("Schema::get_object_mut") + } + + pub(crate) fn get_field(&self, field_id: StoredFieldId) -> &StoredField { + self.stored_fields.get(field_id.0).unwrap() + } + + pub(crate) fn get_enum(&self, enum_id: EnumId) -> &StoredEnum { + self.stored_enums.get(enum_id.0).unwrap() + } + + pub(crate) fn get_scalar(&self, scalar_id: ScalarId) -> &StoredScalar { + self.stored_scalars.get(scalar_id.0).unwrap() + } + + pub(crate) fn get_union(&self, union_id: UnionId) -> &StoredUnion { + self.stored_unions + .get(union_id.0) + .expect("Schema::get_union") + } + + fn find_interface(&self, interface_name: &str) -> InterfaceId { + self.find_type_id(interface_name).as_interface_id().unwrap() + } + + pub(crate) fn find_type(&self, type_name: &str) -> Option { + self.names.get(type_name).copied() + } + + pub(crate) fn objects(&self) -> impl Iterator { + self.stored_objects .iter() - .find(|&&s| s == typename_) - .map(|_| ()) - .or_else(|| { - self.enums - .get(typename_) - .map(|enm| enm.is_required.set(true)) - }) - .or_else(|| self.inputs.get(typename_).map(|input| input.require(self))) - .or_else(|| { - self.objects - .get(typename_) - .map(|object| object.require(self)) - }) - .or_else(|| { - self.scalars - .get(typename_) - .map(|scalar| scalar.is_required.set(true)) - }); + .enumerate() + .map(|(idx, obj)| (ObjectId(idx as u32), obj)) + } + + pub(crate) fn inputs(&self) -> impl Iterator { + self.stored_inputs + .iter() + .enumerate() + .map(|(idx, obj)| (InputId(idx as u32), obj)) } - pub(crate) fn contains_scalar(&self, type_name: &str) -> bool { - DEFAULT_SCALARS.iter().any(|s| s == &type_name) || self.scalars.contains_key(type_name) - } - - pub(crate) fn fragment_target( - &self, - target_name: &str, - ) -> Option> { - self.objects - .get(target_name) - .map(crate::fragments::FragmentTarget::Object) - .or_else(|| { - self.interfaces - .get(target_name) - .map(crate::fragments::FragmentTarget::Interface) - }) - .or_else(|| { - self.unions - .get(target_name) - .map(crate::fragments::FragmentTarget::Union) - }) + fn find_type_id(&self, type_name: &str) -> TypeId { + match self.names.get(type_name) { + Some(id) => *id, + None => { + panic!( + "graphql-client-codegen internal error: failed to resolve TypeId for `{}°.", + type_name + ); + } + } } } -impl<'schema> std::convert::From<&'schema graphql_parser::schema::Document> for Schema<'schema> { - fn from(ast: &'schema graphql_parser::schema::Document) -> Schema<'schema> { - let mut schema = Schema::new(); - - // Holds which objects implement which interfaces so we can populate GqlInterface#implemented_by later. - // It maps interface names to a vec of implementation names. - let mut interface_implementations: BTreeMap<&str, Vec<&str>> = BTreeMap::new(); - - for definition in &ast.definitions { - match definition { - schema::Definition::TypeDefinition(ty_definition) => match ty_definition { - schema::TypeDefinition::Object(obj) => { - for implementing in &obj.implements_interfaces { - let name = &obj.name; - interface_implementations - .entry(implementing) - .and_modify(|objects| objects.push(name)) - .or_insert_with(|| vec![name]); - } - - schema - .objects - .insert(&obj.name, GqlObject::from_graphql_parser_object(&obj)); - } - schema::TypeDefinition::Enum(enm) => { - schema.enums.insert( - &enm.name, - GqlEnum { - name: &enm.name, - description: enm.description.as_ref().map(String::as_str), - variants: enm - .values - .iter() - .map(|v| EnumVariant { - description: v.description.as_ref().map(String::as_str), - name: &v.name, - }) - .collect(), - is_required: false.into(), - }, - ); - } - schema::TypeDefinition::Scalar(scalar) => { - schema.scalars.insert( - &scalar.name, - Scalar { - name: &scalar.name, - description: scalar.description.as_ref().map(String::as_str), - is_required: false.into(), - }, - ); - } - schema::TypeDefinition::Union(union) => { - let variants: BTreeSet<&str> = - union.types.iter().map(String::as_str).collect(); - schema.unions.insert( - &union.name, - GqlUnion { - name: &union.name, - variants, - description: union.description.as_ref().map(String::as_str), - is_required: false.into(), - }, - ); - } - schema::TypeDefinition::Interface(interface) => { - let mut iface = GqlInterface::new( - &interface.name, - interface.description.as_ref().map(String::as_str), - ); - iface - .fields - .extend(interface.fields.iter().map(|f| GqlObjectField { - description: f.description.as_ref().map(String::as_str), - name: f.name.as_str(), - type_: FieldType::from(&f.field_type), - deprecation: DeprecationStatus::Current, - })); - schema.interfaces.insert(&interface.name, iface); - } - schema::TypeDefinition::InputObject(input) => { - schema.inputs.insert(&input.name, GqlInput::from(input)); +impl StoredInputType { + pub(crate) fn used_input_ids_recursive(&self, used_types: &mut UsedTypes, schema: &Schema) { + for type_id in self.fields.iter().map(|(_name, ty)| ty.id) { + match type_id { + TypeId::Input(input_id) => { + if used_types.types.contains(&type_id) { + continue; + } else { + used_types.types.insert(type_id); + let input = schema.get_input(input_id); + input.used_input_ids_recursive(used_types, schema); } - }, - schema::Definition::DirectiveDefinition(_) => (), - schema::Definition::TypeExtension(_extension) => (), - schema::Definition::SchemaDefinition(definition) => { - schema.query_type = definition.query.as_ref().map(String::as_str); - schema.mutation_type = definition.mutation.as_ref().map(String::as_str); - schema.subscription_type = definition.subscription.as_ref().map(String::as_str); } + TypeId::Enum(_) | TypeId::Scalar(_) => { + used_types.types.insert(type_id); + } + _ => (), } } + } - schema - .ingest_interface_implementations(interface_implementations) - .expect("schema ingestion"); + fn contains_type_without_indirection<'a>( + &'a self, + input_id: InputId, + schema: &'a Schema, + visited_types: &mut BTreeSet<&'a str>, + ) -> bool { + visited_types.insert(&self.name); + // The input type is recursive if any of its members contains it, without indirection + self.fields.iter().any(|(_name, field_type)| { + // the field is indirected, so no boxing is needed + if field_type.is_indirected() { + return false; + } - schema + let field_input_id = field_type.id.as_input_id(); + + if let Some(field_input_id) = field_input_id { + if field_input_id == input_id { + return true; + } + + let input = schema.get_input(field_input_id); + + // no need to visit type twice (prevents infinite recursion) + if visited_types.contains(&input.name.as_str()) { + return false; + } + + // we check if the other input contains this one (without indirection) + input.contains_type_without_indirection(input_id, schema, visited_types) + } else { + // the field is not referring to an input type + false + } + }) } } -impl<'schema> - std::convert::From< - &'schema graphql_introspection_query::introspection_response::IntrospectionResponse, - > for Schema<'schema> +pub(crate) fn input_is_recursive_without_indirection(input_id: InputId, schema: &Schema) -> bool { + let input = schema.get_input(input_id); + let mut visited_types = BTreeSet::<&str>::new(); + input.contains_type_without_indirection(input_id, schema, &mut visited_types) +} +impl<'doc, T> std::convert::From> for Schema +where + T: graphql_parser::query::Text<'doc>, + T::Value: AsRef, +{ + fn from(ast: graphql_parser::schema::Document<'doc, T>) -> Schema { + graphql_parser_conversion::build_schema(ast) + } +} + +impl std::convert::From + for Schema { fn from( - src: &'schema graphql_introspection_query::introspection_response::IntrospectionResponse, + src: graphql_introspection_query::introspection_response::IntrospectionResponse, ) -> Self { - use graphql_introspection_query::introspection_response::__TypeKind; - - let mut schema = Schema::new(); - let root = src - .as_schema() - .schema - .as_ref() - .expect("__schema is not null"); - - schema.query_type = root - .query_type - .as_ref() - .and_then(|ty| ty.name.as_ref()) - .map(String::as_str); - schema.mutation_type = root - .mutation_type - .as_ref() - .and_then(|ty| ty.name.as_ref()) - .map(String::as_str); - schema.subscription_type = root - .subscription_type - .as_ref() - .and_then(|ty| ty.name.as_ref()) - .map(String::as_str); - - // Holds which objects implement which interfaces so we can populate GqlInterface#implemented_by later. - // It maps interface names to a vec of implementation names. - let mut interface_implementations: BTreeMap<&str, Vec<&str>> = BTreeMap::new(); - - for ty in root - .types - .as_ref() - .expect("types in schema") - .iter() - .filter_map(|t| t.as_ref().map(|t| &t.full_type)) - { - let name: &str = ty - .name - .as_ref() - .map(String::as_str) - .expect("type definition name"); - - match ty.kind { - Some(__TypeKind::ENUM) => { - let variants: Vec> = ty - .enum_values - .as_ref() - .expect("enum variants") - .iter() - .map(|t| { - t.as_ref().map(|t| EnumVariant { - description: t.description.as_ref().map(String::as_str), - name: t - .name - .as_ref() - .map(String::as_str) - .expect("enum variant name"), - }) - }) - .filter_map(|t| t) - .collect(); - let enm = GqlEnum { - name, - description: ty.description.as_ref().map(String::as_str), - variants, - is_required: false.into(), - }; - schema.enums.insert(name, enm); - } - Some(__TypeKind::SCALAR) => { - if DEFAULT_SCALARS.iter().find(|s| s == &&name).is_none() { - schema.scalars.insert( - name, - Scalar { - name, - description: ty.description.as_ref().map(String::as_str), - is_required: false.into(), - }, - ); - } - } - Some(__TypeKind::UNION) => { - let variants: BTreeSet<&str> = ty - .possible_types - .as_ref() - .unwrap() - .iter() - .filter_map(|t| { - t.as_ref() - .and_then(|t| t.type_ref.name.as_ref().map(String::as_str)) - }) - .collect(); - schema.unions.insert( - name, - GqlUnion { - name: ty.name.as_ref().map(String::as_str).expect("unnamed union"), - description: ty.description.as_ref().map(String::as_str), - variants, - is_required: false.into(), - }, - ); - } - Some(__TypeKind::OBJECT) => { - for implementing in ty - .interfaces - .as_ref() - .map(Vec::as_slice) - .unwrap_or_else(|| &[]) - .iter() - .filter_map(Option::as_ref) - .map(|t| &t.type_ref.name) - { - interface_implementations - .entry( - implementing - .as_ref() - .map(String::as_str) - .expect("interface name"), - ) - .and_modify(|objects| objects.push(name)) - .or_insert_with(|| vec![name]); - } + json_conversion::build_schema(src) + } +} - schema - .objects - .insert(name, GqlObject::from_introspected_schema_json(ty)); - } - Some(__TypeKind::INTERFACE) => { - let mut iface = - GqlInterface::new(name, ty.description.as_ref().map(String::as_str)); - iface.fields.extend( - ty.fields - .as_ref() - .expect("interface fields") - .iter() - .filter_map(Option::as_ref) - .map(|f| GqlObjectField { - description: f.description.as_ref().map(String::as_str), - name: f.name.as_ref().expect("field name").as_str(), - type_: FieldType::from(f.type_.as_ref().expect("field type")), - deprecation: DeprecationStatus::Current, - }), - ); - schema.interfaces.insert(name, iface); - } - Some(__TypeKind::INPUT_OBJECT) => { - schema.inputs.insert(name, GqlInput::from(ty)); +pub(crate) fn resolve_field_type<'doc, T>( + schema: &Schema, + inner: &graphql_parser::schema::Type<'doc, T>, +) -> StoredFieldType +where + T: graphql_parser::query::Text<'doc>, +{ + use crate::type_qualifiers::graphql_parser_depth; + use graphql_parser::schema::Type::*; + + let qualifiers_depth = graphql_parser_depth(inner); + let mut qualifiers = Vec::with_capacity(qualifiers_depth); + + let mut inner = inner; + + loop { + match inner { + ListType(new_inner) => { + qualifiers.push(GraphqlTypeQualifier::List); + inner = new_inner; + } + NonNullType(new_inner) => { + qualifiers.push(GraphqlTypeQualifier::Required); + inner = new_inner; + } + NamedType(name) => { + return StoredFieldType { + id: schema.find_type_id(name.as_ref()), + qualifiers, } - _ => unimplemented!("unimplemented definition"), } } - - schema - .ingest_interface_implementations(interface_implementations) - .expect("schema ingestion"); - - schema } } -pub(crate) enum ParsedSchema { - GraphQLParser(graphql_parser::schema::Document), - Json(graphql_introspection_query::introspection_response::IntrospectionResponse), +pub(crate) trait ObjectLike { + fn name(&self) -> &str; + + fn get_field_by_name<'a>( + &'a self, + name: &str, + schema: &'a Schema, + ) -> Option<(StoredFieldId, &'a StoredField)>; } -impl<'schema> From<&'schema ParsedSchema> for Schema<'schema> { - fn from(parsed_schema: &'schema ParsedSchema) -> Schema<'schema> { - match parsed_schema { - ParsedSchema::GraphQLParser(s) => s.into(), - ParsedSchema::Json(s) => s.into(), - } +impl ObjectLike for StoredObject { + fn name(&self) -> &str { + &self.name + } + + fn get_field_by_name<'a>( + &'a self, + name: &str, + schema: &'a Schema, + ) -> Option<(StoredFieldId, &'a StoredField)> { + self.fields + .iter() + .map(|field_id| (*field_id, schema.get_field(*field_id))) + .find(|(_, f)| f.name == name) } } -#[cfg(test)] -mod tests { - use super::*; - use crate::constants::*; - - #[test] - fn build_schema_works() { - let gql_schema = include_str!("tests/star_wars_schema.graphql"); - let gql_schema = graphql_parser::parse_schema(gql_schema).unwrap(); - let built = Schema::from(&gql_schema); - assert_eq!( - built.objects.get("Droid"), - Some(&GqlObject { - description: None, - name: "Droid", - fields: vec![ - GqlObjectField { - description: None, - name: TYPENAME_FIELD, - type_: FieldType::new(string_type()), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "id", - type_: FieldType::new("ID").nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "name", - type_: FieldType::new("String").nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "friends", - type_: FieldType::new("Character").list(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "friendsConnection", - type_: FieldType::new("FriendsConnection").nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "appearsIn", - type_: FieldType::new("Episode").list().nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "primaryFunction", - type_: FieldType::new("String"), - deprecation: DeprecationStatus::Current, - }, - ], - is_required: false.into(), - }) - ) +impl ObjectLike for StoredInterface { + fn name(&self) -> &str { + &self.name + } + + fn get_field_by_name<'a>( + &'a self, + name: &str, + schema: &'a Schema, + ) -> Option<(StoredFieldId, &'a StoredField)> { + self.fields + .iter() + .map(|field_id| (*field_id, schema.get_field(*field_id))) + .find(|(_, field)| field.name == name) } } diff --git a/graphql_client_codegen/src/schema/graphql_parser_conversion.rs b/graphql_client_codegen/src/schema/graphql_parser_conversion.rs new file mode 100644 index 000000000..b235e7ae9 --- /dev/null +++ b/graphql_client_codegen/src/schema/graphql_parser_conversion.rs @@ -0,0 +1,427 @@ +use super::{Schema, StoredInputFieldType, TypeId}; +use crate::schema::resolve_field_type; +use graphql_parser::schema::{ + self as parser, Definition, Document, TypeDefinition, TypeExtension, UnionType, +}; + +pub(super) fn build_schema<'doc, T>( + mut src: graphql_parser::schema::Document<'doc, T>, +) -> super::Schema +where + T: graphql_parser::query::Text<'doc>, + T::Value: AsRef, +{ + let mut schema = Schema::new(); + convert(&mut src, &mut schema); + schema +} + +fn convert<'doc, T>(src: &mut graphql_parser::schema::Document<'doc, T>, schema: &mut Schema) +where + T: graphql_parser::query::Text<'doc>, + T::Value: AsRef, +{ + populate_names_map(schema, &src.definitions); + + src.definitions + .iter_mut() + .filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Scalar(scalar)) => Some(scalar), + _ => None, + }) + .for_each(|scalar| ingest_scalar(schema, scalar)); + + enums_mut(src).for_each(|enm| ingest_enum(schema, enm)); + + unions_mut(src).for_each(|union| ingest_union(schema, union)); + + interfaces_mut(src).for_each(|iface| ingest_interface(schema, iface)); + + objects_mut(src).for_each(|obj| ingest_object(schema, obj)); + extend_object_type_extensions_mut(src) + .for_each(|ext| ingest_object_type_extension(schema, ext)); + + inputs_mut(src).for_each(|input| ingest_input(schema, input)); + + let schema_definition = src.definitions.iter_mut().find_map(|def| match def { + Definition::SchemaDefinition(definition) => Some(definition), + _ => None, + }); + + if let Some(schema_definition) = schema_definition { + schema.query_type = schema_definition + .query + .as_mut() + .and_then(|n| schema.names.get(n.as_ref())) + .and_then(|id| id.as_object_id()); + schema.mutation_type = schema_definition + .mutation + .as_mut() + .and_then(|n| schema.names.get(n.as_ref())) + .and_then(|id| id.as_object_id()); + schema.subscription_type = schema_definition + .subscription + .as_mut() + .and_then(|n| schema.names.get(n.as_ref())) + .and_then(|id| id.as_object_id()); + } else { + schema.query_type = schema.names.get("Query").and_then(|id| id.as_object_id()); + + schema.mutation_type = schema + .names + .get("Mutation") + .and_then(|id| id.as_object_id()); + + schema.subscription_type = schema + .names + .get("Subscription") + .and_then(|id| id.as_object_id()); + }; +} + +fn populate_names_map<'doc, T>(schema: &mut Schema, definitions: &[Definition<'doc, T>]) +where + T: graphql_parser::query::Text<'doc>, +{ + definitions + .iter() + .filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Enum(enm)) => Some(enm.name.as_ref()), + _ => None, + }) + .enumerate() + .for_each(|(idx, enum_name)| { + schema.names.insert(enum_name.into(), TypeId::r#enum(idx)); + }); + + definitions + .iter() + .filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Object(object)) => { + Some(object.name.as_ref()) + } + _ => None, + }) + .enumerate() + .for_each(|(idx, object_name)| { + schema + .names + .insert(object_name.into(), TypeId::r#object(idx as u32)); + }); + + definitions + .iter() + .filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Interface(interface)) => { + Some(interface.name.as_ref()) + } + _ => None, + }) + .enumerate() + .for_each(|(idx, interface_name)| { + schema + .names + .insert(interface_name.into(), TypeId::interface(idx)); + }); + + definitions + .iter() + .filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Union(union)) => Some(union.name.as_ref()), + _ => None, + }) + .enumerate() + .for_each(|(idx, union_name)| { + schema.names.insert(union_name.into(), TypeId::union(idx)); + }); + + definitions + .iter() + .filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::InputObject(input)) => { + Some(input.name.as_ref()) + } + _ => None, + }) + .enumerate() + .for_each(|(idx, input_name)| { + schema + .names + .insert(input_name.into(), TypeId::input(idx as u32)); + }); +} + +fn ingest_union<'doc, T>(schema: &mut Schema, union: &mut UnionType<'doc, T>) +where + T: graphql_parser::query::Text<'doc>, +{ + let stored_union = super::StoredUnion { + name: union.name.as_ref().into(), + variants: union + .types + .iter() + .map(|name| schema.find_type_id(name.as_ref())) + .collect(), + }; + + schema.stored_unions.push(stored_union); +} + +fn ingest_object<'doc, T>( + schema: &mut Schema, + obj: &mut graphql_parser::schema::ObjectType<'doc, T>, +) where + T: graphql_parser::query::Text<'doc>, +{ + let object_id = schema + .find_type_id(obj.name.as_ref()) + .as_object_id() + .unwrap(); + let mut field_ids = Vec::with_capacity(obj.fields.len()); + + for field in obj.fields.iter_mut() { + let field = super::StoredField { + name: field.name.as_ref().into(), + r#type: resolve_field_type(schema, &field.field_type), + parent: super::StoredFieldParent::Object(object_id), + deprecation: find_deprecation(&field.directives), + }; + + field_ids.push(schema.push_field(field)); + } + + // Ingest the object itself + let object = super::StoredObject { + name: obj.name.as_ref().into(), + fields: field_ids, + implements_interfaces: obj + .implements_interfaces + .iter() + .map(|iface_name| schema.find_interface(iface_name.as_ref())) + .collect(), + }; + + schema.push_object(object); +} + +fn ingest_object_type_extension<'doc, T>( + schema: &mut Schema, + ext: &mut graphql_parser::schema::ObjectTypeExtension<'doc, T>, +) where + T: graphql_parser::query::Text<'doc>, +{ + let object_id = schema + .find_type_id(ext.name.as_ref()) + .as_object_id() + .unwrap(); + let mut field_ids = Vec::with_capacity(ext.fields.len()); + + for field in ext.fields.iter_mut() { + let field = super::StoredField { + name: field.name.as_ref().into(), + r#type: resolve_field_type(schema, &field.field_type), + parent: super::StoredFieldParent::Object(object_id), + deprecation: find_deprecation(&field.directives), + }; + + field_ids.push(schema.push_field(field)); + } + + let iface_ids = ext + .implements_interfaces + .iter() + .map(|iface_name| schema.find_interface(iface_name.as_ref())) + .collect::>(); + + let object = schema.get_object_mut(object_id); + object.implements_interfaces.extend(iface_ids); + object.fields.extend(field_ids); +} + +fn ingest_scalar<'doc, T>( + schema: &mut Schema, + scalar: &mut graphql_parser::schema::ScalarType<'doc, T>, +) where + T: graphql_parser::query::Text<'doc>, +{ + let name: String = scalar.name.as_ref().into(); + let name_for_names = name.clone(); + + let scalar = super::StoredScalar { name }; + + let scalar_id = schema.push_scalar(scalar); + + schema + .names + .insert(name_for_names, TypeId::Scalar(scalar_id)); +} + +fn ingest_enum<'doc, T>(schema: &mut Schema, enm: &mut graphql_parser::schema::EnumType<'doc, T>) +where + T: graphql_parser::query::Text<'doc>, +{ + let enm = super::StoredEnum { + name: enm.name.as_ref().into(), + variants: enm + .values + .iter_mut() + .map(|value| value.name.as_ref().into()) + .collect(), + }; + + schema.push_enum(enm); +} + +fn ingest_interface<'doc, T>( + schema: &mut Schema, + interface: &mut graphql_parser::schema::InterfaceType<'doc, T>, +) where + T: graphql_parser::query::Text<'doc>, +{ + let interface_id = schema + .find_type_id(interface.name.as_ref()) + .as_interface_id() + .unwrap(); + + let mut field_ids = Vec::with_capacity(interface.fields.len()); + + for field in interface.fields.iter_mut() { + let field = super::StoredField { + name: field.name.as_ref().into(), + r#type: resolve_field_type(schema, &field.field_type), + parent: super::StoredFieldParent::Interface(interface_id), + deprecation: find_deprecation(&field.directives), + }; + + field_ids.push(schema.push_field(field)); + } + + let new_interface = super::StoredInterface { + name: interface.name.as_ref().into(), + fields: field_ids, + }; + + schema.push_interface(new_interface); +} + +fn find_deprecation<'doc, T>(directives: &[parser::Directive<'doc, T>]) -> Option> +where + T: graphql_parser::query::Text<'doc>, +{ + directives + .iter() + .find(|directive| directive.name.as_ref() == "deprecated") + .map(|directive| { + directive + .arguments + .iter() + .find(|(name, _)| name.as_ref() == "reason") + .and_then(|(_, value)| match value { + graphql_parser::query::Value::String(s) => Some(s.clone()), + _ => None, + }) + }) +} + +fn ingest_input<'doc, T>(schema: &mut Schema, input: &mut parser::InputObjectType<'doc, T>) +where + T: graphql_parser::query::Text<'doc>, +{ + let is_one_of = input + .directives + .iter() + .any(|directive| directive.name.as_ref() == "oneOf"); + + let input = super::StoredInputType { + name: input.name.as_ref().into(), + fields: input + .fields + .iter_mut() + .map(|val| { + let field_type = super::resolve_field_type(schema, &val.value_type); + ( + val.name.as_ref().into(), + StoredInputFieldType { + qualifiers: field_type.qualifiers, + id: field_type.id, + }, + ) + }) + .collect(), + is_one_of, + }; + + schema.stored_inputs.push(input); +} + +fn objects_mut<'a, 'doc: 'a, T>( + doc: &'a mut Document<'doc, T>, +) -> impl Iterator> +where + T: graphql_parser::query::Text<'doc>, +{ + doc.definitions.iter_mut().filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Object(obj)) => Some(obj), + _ => None, + }) +} + +fn extend_object_type_extensions_mut<'a, 'doc: 'a, T>( + doc: &'a mut Document<'doc, T>, +) -> impl Iterator> +where + T: graphql_parser::query::Text<'doc>, +{ + doc.definitions.iter_mut().filter_map(|def| match def { + Definition::TypeExtension(TypeExtension::Object(obj)) => Some(obj), + _ => None, + }) +} + +fn interfaces_mut<'a, 'doc: 'a, T>( + doc: &'a mut Document<'doc, T>, +) -> impl Iterator> +where + T: graphql_parser::query::Text<'doc>, +{ + doc.definitions.iter_mut().filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Interface(interface)) => Some(interface), + _ => None, + }) +} + +fn unions_mut<'a, 'doc: 'a, T>( + doc: &'a mut Document<'doc, T>, +) -> impl Iterator> +where + T: graphql_parser::query::Text<'doc>, +{ + doc.definitions.iter_mut().filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Union(union)) => Some(union), + _ => None, + }) +} + +fn enums_mut<'a, 'doc: 'a, T>( + doc: &'a mut Document<'doc, T>, +) -> impl Iterator> +where + T: graphql_parser::query::Text<'doc>, +{ + doc.definitions.iter_mut().filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::Enum(r#enum)) => Some(r#enum), + _ => None, + }) +} + +fn inputs_mut<'a, 'doc: 'a, T>( + doc: &'a mut Document<'doc, T>, +) -> impl Iterator> +where + T: graphql_parser::query::Text<'doc>, +{ + doc.definitions.iter_mut().filter_map(|def| match def { + Definition::TypeDefinition(TypeDefinition::InputObject(input)) => Some(input), + _ => None, + }) +} diff --git a/graphql_client_codegen/src/schema/json_conversion.rs b/graphql_client_codegen/src/schema/json_conversion.rs new file mode 100644 index 000000000..18dea687e --- /dev/null +++ b/graphql_client_codegen/src/schema/json_conversion.rs @@ -0,0 +1,366 @@ +use super::{Schema, TypeId}; +use graphql_introspection_query::introspection_response::{ + FullType, IntrospectionResponse, Schema as JsonSchema, TypeRef, __TypeKind, +}; + +pub(super) fn build_schema(src: IntrospectionResponse) -> Schema { + let mut src = src.into_schema().schema.expect("could not find schema"); + let mut schema = Schema::new(); + build_names_map(&mut src, &mut schema); + convert(&mut src, &mut schema); + + schema +} + +fn build_names_map(src: &mut JsonSchema, schema: &mut Schema) { + let names = &mut schema.names; + + unions_mut(src) + .map(|u| u.name.as_ref().expect("union name")) + .enumerate() + .for_each(|(idx, name)| { + names.insert(name.clone(), TypeId::union(idx)); + }); + + interfaces_mut(src) + .map(|iface| iface.name.as_ref().expect("interface name")) + .enumerate() + .for_each(|(idx, name)| { + names.insert(name.clone(), TypeId::interface(idx)); + }); + + objects_mut(src) + .map(|obj| obj.name.as_ref().expect("object name")) + .enumerate() + .for_each(|(idx, name)| { + names.insert(name.clone(), TypeId::object(idx as u32)); + }); + + inputs_mut(src) + .map(|obj| obj.name.as_ref().expect("input name")) + .enumerate() + .for_each(|(idx, name)| { + names.insert(name.clone(), TypeId::input(idx as u32)); + }); +} + +fn convert(src: &mut JsonSchema, schema: &mut Schema) { + for scalar in scalars_mut(src) { + ingest_scalar(schema, scalar); + } + + for enm in enums_mut(src) { + ingest_enum(schema, enm) + } + + for interface in interfaces_mut(src) { + ingest_interface(schema, interface); + } + + for object in objects_mut(src) { + ingest_object(schema, object); + } + + for unn in unions_mut(src) { + ingest_union(schema, unn) + } + + for input in inputs_mut(src) { + ingest_input(schema, input); + } + + // Define the root operations. + { + schema.query_type = src + .query_type + .as_mut() + .and_then(|n| n.name.as_mut()) + .and_then(|n| schema.names.get(n)) + .and_then(|id| id.as_object_id()); + schema.mutation_type = src + .mutation_type + .as_mut() + .and_then(|n| n.name.as_mut()) + .and_then(|n| schema.names.get(n)) + .and_then(|id| id.as_object_id()); + schema.subscription_type = src + .subscription_type + .as_mut() + .and_then(|n| n.name.as_mut()) + .and_then(|n| schema.names.get(n)) + .and_then(|id| id.as_object_id()); + } +} + +fn types_mut(schema: &mut JsonSchema) -> impl Iterator { + schema + .types + .as_mut() + .expect("schema.types.as_mut()") + .iter_mut() + .filter_map(|t| -> Option<&mut FullType> { t.as_mut().map(|f| &mut f.full_type) }) +} + +fn objects_mut(schema: &mut JsonSchema) -> impl Iterator { + types_mut(schema).filter(|t| t.kind == Some(__TypeKind::OBJECT)) +} + +fn enums_mut(schema: &mut JsonSchema) -> impl Iterator { + types_mut(schema).filter(|t| t.kind == Some(__TypeKind::ENUM)) +} + +fn interfaces_mut(schema: &mut JsonSchema) -> impl Iterator { + types_mut(schema).filter(|t| t.kind == Some(__TypeKind::INTERFACE)) +} + +fn unions_mut(schema: &mut JsonSchema) -> impl Iterator { + types_mut(schema).filter(|t| t.kind == Some(__TypeKind::UNION)) +} + +fn inputs_mut(schema: &mut JsonSchema) -> impl Iterator { + types_mut(schema).filter(|t| t.kind == Some(__TypeKind::INPUT_OBJECT)) +} + +fn scalars_mut(schema: &mut JsonSchema) -> impl Iterator { + types_mut(schema).filter(|t| { + t.kind == Some(__TypeKind::SCALAR) + && !super::DEFAULT_SCALARS.contains(&t.name.as_deref().expect("FullType.name")) + }) +} + +fn ingest_scalar(schema: &mut Schema, scalar: &mut FullType) { + let name: String = scalar.name.take().expect("scalar.name"); + let names_name = name.clone(); + + let id = schema.push_scalar(super::StoredScalar { name }); + + schema.names.insert(names_name, TypeId::Scalar(id)); +} + +fn ingest_enum(schema: &mut Schema, enm: &mut FullType) { + let name = enm.name.take().expect("enm.name"); + let names_name = name.clone(); + + let variants = enm + .enum_values + .as_mut() + .expect("enm.enum_values.as_mut()") + .iter_mut() + .map(|v| { + std::mem::take( + v.name + .as_mut() + .take() + .expect("variant.name.as_mut().take()"), + ) + }) + .collect(); + + let enm = super::StoredEnum { name, variants }; + + let id = schema.push_enum(enm); + + schema.names.insert(names_name, TypeId::Enum(id)); +} + +fn ingest_interface(schema: &mut Schema, iface: &mut FullType) { + let interface_id = schema + .find_type_id(iface.name.as_ref().expect("iface.name")) + .as_interface_id() + .expect("iface type id as interface id"); + let fields = iface.fields.as_mut().expect("interface.fields"); + let mut field_ids = Vec::with_capacity(fields.len()); + + for field in fields.iter_mut() { + let field = super::StoredField { + parent: super::StoredFieldParent::Interface(interface_id), + name: field.name.take().expect("take field name"), + r#type: resolve_field_type( + schema, + &mut field.type_.as_mut().expect("take field type").type_ref, + ), + deprecation: if let Some(true) = field.is_deprecated { + Some(field.deprecation_reason.clone()) + } else { + None + }, + }; + + field_ids.push(schema.push_field(field)); + } + + let interface = super::StoredInterface { + name: std::mem::take(iface.name.as_mut().expect("iface.name.as_mut")), + fields: field_ids, + }; + + schema.push_interface(interface); +} + +fn ingest_object(schema: &mut Schema, object: &mut FullType) { + let object_id = schema + .find_type_id(object.name.as_ref().expect("object.name")) + .as_object_id() + .expect("ingest_object > as_object_id"); + + let fields = object.fields.as_mut().expect("object.fields.as_mut()"); + let mut field_ids = Vec::with_capacity(fields.len()); + + for field in fields.iter_mut() { + let field = super::StoredField { + parent: super::StoredFieldParent::Object(object_id), + name: field.name.take().expect("take field name"), + r#type: resolve_field_type( + schema, + &mut field.type_.as_mut().expect("take field type").type_ref, + ), + deprecation: if let Some(true) = field.is_deprecated { + Some(field.deprecation_reason.clone()) + } else { + None + }, + }; + + field_ids.push(schema.push_field(field)); + } + + let object = super::StoredObject { + name: object.name.take().expect("take object name"), + implements_interfaces: object + .interfaces + .as_ref() + .map(|ifaces| { + ifaces + .iter() + .map(|iface| { + schema + .names + .get(iface.type_ref.name.as_ref().unwrap()) + .and_then(|type_id| type_id.as_interface_id()) + .ok_or_else(|| { + format!( + "Unknown interface: {}", + iface.type_ref.name.as_ref().unwrap() + ) + }) + .unwrap() + }) + .collect() + }) + .unwrap_or_default(), + fields: field_ids, + }; + + schema.push_object(object); +} + +fn ingest_union(schema: &mut Schema, union: &mut FullType) { + let variants = union + .possible_types + .as_ref() + .expect("union.possible_types") + .iter() + .map(|variant| { + schema.find_type_id( + variant + .type_ref + .name + .as_ref() + .expect("variant.type_ref.name"), + ) + }) + .collect(); + let un = super::StoredUnion { + name: union.name.take().expect("union.name.take"), + variants, + }; + + schema.stored_unions.push(un); +} + +fn ingest_input(schema: &mut Schema, input: &mut FullType) { + let mut fields = Vec::new(); + + for field in input + .input_fields + .as_mut() + .expect("Missing input_fields on input") + .iter_mut() + { + fields.push(( + std::mem::take(&mut field.input_value.name), + resolve_input_field_type(schema, &mut field.input_value.type_), + )); + } + + let input = super::StoredInputType { + fields, + name: input.name.take().expect("Input without a name"), + // The one-of input spec is not stable yet, thus the introspection query does not have + // `isOneOf`, so this is always false. + is_one_of: false, + }; + + schema.stored_inputs.push(input); +} + +fn resolve_field_type(schema: &mut Schema, typeref: &mut TypeRef) -> super::StoredFieldType { + from_json_type_inner(schema, typeref) +} + +fn resolve_input_field_type( + schema: &mut Schema, + typeref: &mut TypeRef, +) -> super::StoredInputFieldType { + let field_type = from_json_type_inner(schema, typeref); + + super::StoredInputFieldType { + id: field_type.id, + qualifiers: field_type.qualifiers, + } +} + +fn json_type_qualifiers_depth(typeref: &mut TypeRef) -> usize { + use graphql_introspection_query::introspection_response::*; + + match (typeref.kind.as_mut(), typeref.of_type.as_mut()) { + (Some(__TypeKind::NON_NULL), Some(inner)) => 1 + json_type_qualifiers_depth(inner), + (Some(__TypeKind::LIST), Some(inner)) => 1 + json_type_qualifiers_depth(inner), + (Some(_), None) => 0, + _ => panic!("Non-convertible type in JSON schema: {:?}", typeref), + } +} + +fn from_json_type_inner(schema: &mut Schema, inner: &mut TypeRef) -> super::StoredFieldType { + use crate::type_qualifiers::GraphqlTypeQualifier; + use graphql_introspection_query::introspection_response::*; + + let qualifiers_depth = json_type_qualifiers_depth(inner); + let mut qualifiers = Vec::with_capacity(qualifiers_depth); + + let mut inner = inner; + + loop { + match ( + inner.kind.as_mut(), + inner.of_type.as_mut(), + inner.name.as_mut(), + ) { + (Some(__TypeKind::NON_NULL), Some(new_inner), _) => { + qualifiers.push(GraphqlTypeQualifier::Required); + inner = new_inner.as_mut(); + } + (Some(__TypeKind::LIST), Some(new_inner), _) => { + qualifiers.push(GraphqlTypeQualifier::List); + inner = new_inner.as_mut(); + } + (Some(_), None, Some(name)) => { + return super::StoredFieldType { + id: *schema.names.get(name).expect("schema.names.get(name)"), + qualifiers, + } + } + _ => panic!("Non-convertible type in JSON schema"), + } + } +} diff --git a/graphql_client_codegen/src/schema/tests.rs b/graphql_client_codegen/src/schema/tests.rs new file mode 100644 index 000000000..8735b989f --- /dev/null +++ b/graphql_client_codegen/src/schema/tests.rs @@ -0,0 +1,2 @@ +mod extend_object; +mod github; diff --git a/graphql_client_codegen/src/schema/tests/extend_object.rs b/graphql_client_codegen/src/schema/tests/extend_object.rs new file mode 100644 index 000000000..c3f20fdc0 --- /dev/null +++ b/graphql_client_codegen/src/schema/tests/extend_object.rs @@ -0,0 +1,129 @@ +use crate::schema::Schema; + +const SCHEMA_JSON: &str = include_str!("extend_object_schema.json"); +const SCHEMA_GRAPHQL: &str = include_str!("extend_object_schema.graphql"); + +#[test] +fn ast_from_graphql_and_json_produce_the_same_schema() { + let json: graphql_introspection_query::introspection_response::IntrospectionResponse = + serde_json::from_str(SCHEMA_JSON).unwrap(); + let graphql_parser_schema = graphql_parser::parse_schema(SCHEMA_GRAPHQL) + .unwrap() + .into_static(); + let mut json = Schema::from(json); + let mut gql = Schema::from(graphql_parser_schema); + + assert!(vecs_match(&json.stored_scalars, &gql.stored_scalars)); + + // Root objects + { + assert_eq!( + json.get_object(json.query_type()).name, + gql.get_object(gql.query_type()).name + ); + assert_eq!( + json.mutation_type().map(|t| &json.get_object(t).name), + gql.mutation_type().map(|t| &gql.get_object(t).name), + "Mutation types don't match." + ); + assert_eq!( + json.subscription_type().map(|t| &json.get_object(t).name), + gql.subscription_type().map(|t| &gql.get_object(t).name), + "Subscription types don't match." + ); + } + + // Objects + { + let mut json_stored_objects: Vec<_> = json + .stored_objects + .drain(..) + .filter(|obj| !obj.name.starts_with("__")) + .collect(); + + assert_eq!( + json_stored_objects.len(), + gql.stored_objects.len(), + "Objects count matches." + ); + + json_stored_objects.sort_by(|a, b| a.name.cmp(&b.name)); + gql.stored_objects.sort_by(|a, b| a.name.cmp(&b.name)); + + for (j, g) in json_stored_objects + .iter_mut() + .filter(|obj| !obj.name.starts_with("__")) + .zip(gql.stored_objects.iter_mut()) + { + assert_eq!(j.name, g.name); + assert_eq!( + j.implements_interfaces.len(), + g.implements_interfaces.len(), + "{}", + j.name + ); + assert_eq!(j.fields.len(), g.fields.len(), "{}", j.name); + } + } + + // Unions + { + assert_eq!(json.stored_unions.len(), gql.stored_unions.len()); + + json.stored_unions.sort_by(|a, b| a.name.cmp(&b.name)); + gql.stored_unions.sort_by(|a, b| a.name.cmp(&b.name)); + + for (json, gql) in json.stored_unions.iter().zip(gql.stored_unions.iter()) { + assert_eq!(json.variants.len(), gql.variants.len()); + } + } + + // Interfaces + { + assert_eq!(json.stored_interfaces.len(), gql.stored_interfaces.len()); + + json.stored_interfaces.sort_by(|a, b| a.name.cmp(&b.name)); + gql.stored_interfaces.sort_by(|a, b| a.name.cmp(&b.name)); + + for (json, gql) in json + .stored_interfaces + .iter() + .zip(gql.stored_interfaces.iter()) + { + assert_eq!(json.fields.len(), gql.fields.len()); + } + } + + // Input objects + { + json.stored_enums = json + .stored_enums + .drain(..) + .filter(|enm| !enm.name.starts_with("__")) + .collect(); + assert_eq!(json.stored_inputs.len(), gql.stored_inputs.len()); + + json.stored_inputs.sort_by(|a, b| a.name.cmp(&b.name)); + gql.stored_inputs.sort_by(|a, b| a.name.cmp(&b.name)); + + for (json, gql) in json.stored_inputs.iter().zip(gql.stored_inputs.iter()) { + assert_eq!(json.fields.len(), gql.fields.len()); + } + } + + // Enums + { + assert_eq!(json.stored_enums.len(), gql.stored_enums.len()); + + json.stored_enums.sort_by(|a, b| a.name.cmp(&b.name)); + gql.stored_enums.sort_by(|a, b| a.name.cmp(&b.name)); + + for (json, gql) in json.stored_enums.iter().zip(gql.stored_enums.iter()) { + assert_eq!(json.variants.len(), gql.variants.len()); + } + } +} + +fn vecs_match(a: &[T], b: &[T]) -> bool { + a.len() == b.len() && a.iter().all(|a| b.iter().any(|b| a == b)) +} diff --git a/graphql_client_codegen/src/schema/tests/extend_object_schema.graphql b/graphql_client_codegen/src/schema/tests/extend_object_schema.graphql new file mode 100644 index 000000000..5a2be6d5a --- /dev/null +++ b/graphql_client_codegen/src/schema/tests/extend_object_schema.graphql @@ -0,0 +1,11 @@ +schema { + query: Query +} + +type Query { + foo: String +} + +extend type Query { + bar: Int +} diff --git a/graphql_client_codegen/src/schema/tests/extend_object_schema.json b/graphql_client_codegen/src/schema/tests/extend_object_schema.json new file mode 100644 index 000000000..a44bafddc --- /dev/null +++ b/graphql_client_codegen/src/schema/tests/extend_object_schema.json @@ -0,0 +1,1069 @@ +{ + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": null, + "subscriptionType": null, + "types": [ + { + "kind": "OBJECT", + "name": "Query", + "description": null, + "fields": [ + { + "name": "foo", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "bar", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The `Boolean` scalar type represents `true` or `false`.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Schema", + "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", + "fields": [ + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "types", + "description": "A list of all types supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": "The type that query operations will be rooted at.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": "If this server supports mutation, the type that mutation operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": "If this server support subscription, the type that subscription operations will be rooted at.", + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": "A list of all directives supported by this server.", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByUrl`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.", + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "specifiedByUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": "An enum describing what kind of type a given `__Type` is.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": "Indicates this type is a scalar.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Indicates this type is a union. `possibleTypes` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Indicates this type is an enum. `enumValues` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Indicates this type is an input object. `inputFields` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": "Indicates this type is a list. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": "Indicates this type is a non-null. `ofType` is a valid field.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": "A GraphQL-formatted string representing the default value for this input value.", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isRepeatable", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": "A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": "Location adjacent to a query operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": "Location adjacent to a mutation operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "description": "Location adjacent to a subscription operation.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": "Location adjacent to a field.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": "Location adjacent to a fragment definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": "Location adjacent to a fragment spread.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": "Location adjacent to an inline fragment.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VARIABLE_DEFINITION", + "description": "Location adjacent to a variable definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": "Location adjacent to a schema definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": "Location adjacent to a scalar definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": "Location adjacent to an object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": "Location adjacent to a field definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": "Location adjacent to an argument definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": "Location adjacent to an interface definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": "Location adjacent to a union definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": "Location adjacent to an enum definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": "Location adjacent to an enum value definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": "Location adjacent to an input object type definition.", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": "Location adjacent to an input object field definition.", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + } + ], + "directives": [ + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", + "isRepeatable": false, + "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ] + }, + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", + "isRepeatable": false, + "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ] + }, + { + "name": "deprecated", + "description": "Marks an element of a GraphQL schema as no longer supported.", + "isRepeatable": false, + "locations": [ + "FIELD_DEFINITION", + "ARGUMENT_DEFINITION", + "INPUT_FIELD_DEFINITION", + "ENUM_VALUE" + ], + "args": [ + { + "name": "reason", + "description": "Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"No longer supported\"", + "isDeprecated": false, + "deprecationReason": null + } + ] + }, + { + "name": "specifiedBy", + "description": "Exposes a URL that specifies the behaviour of this scalar.", + "isRepeatable": false, + "locations": ["SCALAR"], + "args": [ + { + "name": "url", + "description": "The URL that specifies the behaviour of this scalar.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ] + } + ] + } +} diff --git a/graphql_client_codegen/src/schema/tests/github.rs b/graphql_client_codegen/src/schema/tests/github.rs new file mode 100644 index 000000000..9feee0981 --- /dev/null +++ b/graphql_client_codegen/src/schema/tests/github.rs @@ -0,0 +1,129 @@ +use crate::schema::Schema; + +const SCHEMA_JSON: &str = include_str!("github_schema.json"); +const SCHEMA_GRAPHQL: &str = include_str!("github_schema.graphql"); + +#[test] +fn ast_from_graphql_and_json_produce_the_same_schema() { + let json: graphql_introspection_query::introspection_response::IntrospectionResponse = + serde_json::from_str(SCHEMA_JSON).unwrap(); + let graphql_parser_schema = graphql_parser::parse_schema(SCHEMA_GRAPHQL) + .unwrap() + .into_static(); + let mut json = Schema::from(json); + let mut gql = Schema::from(graphql_parser_schema); + + assert!(vecs_match(&json.stored_scalars, &gql.stored_scalars)); + + // Root objects + { + assert_eq!( + json.get_object(json.query_type()).name, + gql.get_object(gql.query_type()).name + ); + assert_eq!( + json.mutation_type().map(|t| &json.get_object(t).name), + gql.mutation_type().map(|t| &gql.get_object(t).name), + "Mutation types don't match." + ); + assert_eq!( + json.subscription_type().map(|t| &json.get_object(t).name), + gql.subscription_type().map(|t| &gql.get_object(t).name), + "Subscription types don't match." + ); + } + + // Objects + { + let mut json_stored_objects: Vec<_> = json + .stored_objects + .drain(..) + .filter(|obj| !obj.name.starts_with("__")) + .collect(); + + assert_eq!( + json_stored_objects.len(), + gql.stored_objects.len(), + "Objects count matches." + ); + + json_stored_objects.sort_by(|a, b| a.name.cmp(&b.name)); + gql.stored_objects.sort_by(|a, b| a.name.cmp(&b.name)); + + for (j, g) in json_stored_objects + .iter_mut() + .filter(|obj| !obj.name.starts_with("__")) + .zip(gql.stored_objects.iter_mut()) + { + assert_eq!(j.name, g.name); + assert_eq!( + j.implements_interfaces.len(), + g.implements_interfaces.len(), + "{}", + j.name + ); + assert_eq!(j.fields.len(), g.fields.len(), "{}", j.name); + } + } + + // Unions + { + assert_eq!(json.stored_unions.len(), gql.stored_unions.len()); + + json.stored_unions.sort_by(|a, b| a.name.cmp(&b.name)); + gql.stored_unions.sort_by(|a, b| a.name.cmp(&b.name)); + + for (json, gql) in json.stored_unions.iter().zip(gql.stored_unions.iter()) { + assert_eq!(json.variants.len(), gql.variants.len()); + } + } + + // Interfaces + { + assert_eq!(json.stored_interfaces.len(), gql.stored_interfaces.len()); + + json.stored_interfaces.sort_by(|a, b| a.name.cmp(&b.name)); + gql.stored_interfaces.sort_by(|a, b| a.name.cmp(&b.name)); + + for (json, gql) in json + .stored_interfaces + .iter() + .zip(gql.stored_interfaces.iter()) + { + assert_eq!(json.fields.len(), gql.fields.len()); + } + } + + // Input objects + { + json.stored_enums = json + .stored_enums + .drain(..) + .filter(|enm| !enm.name.starts_with("__")) + .collect(); + assert_eq!(json.stored_inputs.len(), gql.stored_inputs.len()); + + json.stored_inputs.sort_by(|a, b| a.name.cmp(&b.name)); + gql.stored_inputs.sort_by(|a, b| a.name.cmp(&b.name)); + + for (json, gql) in json.stored_inputs.iter().zip(gql.stored_inputs.iter()) { + assert_eq!(json.fields.len(), gql.fields.len()); + } + } + + // Enums + { + assert_eq!(json.stored_enums.len(), gql.stored_enums.len()); + + json.stored_enums.sort_by(|a, b| a.name.cmp(&b.name)); + gql.stored_enums.sort_by(|a, b| a.name.cmp(&b.name)); + + for (json, gql) in json.stored_enums.iter().zip(gql.stored_enums.iter()) { + assert_eq!(json.variants.len(), gql.variants.len()); + } + } +} + +fn vecs_match(a: &[T], b: &[T]) -> bool { + a.len() == b.len() && a.iter().all(|a| b.iter().any(|b| a == b)) +} diff --git a/graphql_client_codegen/src/tests/github_schema.graphql b/graphql_client_codegen/src/schema/tests/github_schema.graphql similarity index 99% rename from graphql_client_codegen/src/tests/github_schema.graphql rename to graphql_client_codegen/src/schema/tests/github_schema.graphql index 98a6b0dd6..a73724562 100644 --- a/graphql_client_codegen/src/tests/github_schema.graphql +++ b/graphql_client_codegen/src/schema/tests/github_schema.graphql @@ -2409,7 +2409,7 @@ type IssueTimelineConnection { "An item in an issue timeline" union IssueTimelineItem = - AssignedEvent + | AssignedEvent | ClosedEvent | Commit | CrossReferencedEvent @@ -5008,7 +5008,7 @@ type PullRequestTimelineConnection { "An item in an pull request timeline" union PullRequestTimelineItem = - AssignedEvent + | AssignedEvent | BaseRefForcePushedEvent | ClosedEvent | Commit @@ -6939,7 +6939,7 @@ type ReviewRequestedEvent implements Node { "The results of a search." union SearchResultItem = - Issue + | Issue | MarketplaceListing | Organization | PullRequest diff --git a/graphql_client_codegen/src/tests/github_schema.json b/graphql_client_codegen/src/schema/tests/github_schema.json similarity index 100% rename from graphql_client_codegen/src/tests/github_schema.json rename to graphql_client_codegen/src/schema/tests/github_schema.json diff --git a/graphql_client_codegen/src/selection.rs b/graphql_client_codegen/src/selection.rs deleted file mode 100644 index 5095f4bdf..000000000 --- a/graphql_client_codegen/src/selection.rs +++ /dev/null @@ -1,352 +0,0 @@ -use crate::constants::*; -use failure::*; -use graphql_parser::query::SelectionSet; -use std::collections::BTreeMap; - -/// A single object field as part of a selection. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SelectionField<'query> { - pub alias: Option<&'query str>, - pub name: &'query str, - pub fields: Selection<'query>, -} - -/// A spread fragment in a selection (e.g. `...MyFragment`). -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SelectionFragmentSpread<'query> { - pub fragment_name: &'query str, -} - -/// An inline fragment as part of a selection (e.g. `...on MyThing { name }`). -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SelectionInlineFragment<'query> { - pub on: &'query str, - pub fields: Selection<'query>, -} - -/// An element in a query selection. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum SelectionItem<'query> { - Field(SelectionField<'query>), - FragmentSpread(SelectionFragmentSpread<'query>), - InlineFragment(SelectionInlineFragment<'query>), -} - -impl<'query> SelectionItem<'query> { - pub fn as_typename(&self) -> Option<&SelectionField<'_>> { - if let SelectionItem::Field(f) = self { - if f.name == TYPENAME_FIELD { - return Some(f); - } - } - None - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Selection<'query>(Vec>); - -impl<'query> Selection<'query> { - pub(crate) fn extract_typename<'s, 'context: 's>( - &'s self, - context: &'context crate::query::QueryContext<'_, '_>, - ) -> Option<&SelectionField<'_>> { - // __typename is selected directly - if let Some(field) = self.0.iter().filter_map(SelectionItem::as_typename).next() { - return Some(field); - }; - - // typename is selected through a fragment - (&self) - .into_iter() - .filter_map(|f| match f { - SelectionItem::FragmentSpread(SelectionFragmentSpread { fragment_name }) => { - Some(fragment_name) - } - _ => None, - }) - .filter_map(|fragment_name| { - let fragment = context.fragments.get(fragment_name); - - fragment.and_then(|fragment| fragment.selection.extract_typename(context)) - }) - .next() - } - - // Implementation helper for `selected_variants_on_union`. - fn selected_variants_on_union_inner<'s>( - &'s self, - context: &'s crate::query::QueryContext<'_, '_>, - selected_variants: &mut BTreeMap<&'s str, Selection<'s>>, - // the name of the type the selection applies to - selection_on: &str, - ) -> Result<(), failure::Error> { - for item in self.0.iter() { - match item { - SelectionItem::Field(_) => (), - SelectionItem::InlineFragment(inline_fragment) => { - selected_variants - .entry(inline_fragment.on) - .and_modify(|entry| entry.0.extend(inline_fragment.fields.0.clone())) - .or_insert_with(|| { - let mut items = Vec::with_capacity(inline_fragment.fields.0.len()); - items.extend(inline_fragment.fields.0.clone()); - Selection(items) - }); - } - SelectionItem::FragmentSpread(SelectionFragmentSpread { fragment_name }) => { - let fragment = context - .fragments - .get(fragment_name) - .ok_or_else(|| format_err!("Unknown fragment: {}", &fragment_name))?; - - // The fragment can either be on the union/interface itself, or on one of its variants (type-refining fragment). - if fragment.on.name() == selection_on { - // The fragment is on the union/interface itself. - fragment.selection.selected_variants_on_union_inner( - context, - selected_variants, - selection_on, - )?; - } else { - // Type-refining fragment - selected_variants - .entry(fragment.on.name()) - .and_modify(|entry| entry.0.extend(fragment.selection.0.clone())) - .or_insert_with(|| { - let mut items = Vec::with_capacity(fragment.selection.0.len()); - items.extend(fragment.selection.0.clone()); - Selection(items) - }); - } - } - } - } - - Ok(()) - } - - /// This method should only be invoked on selections on union and interface fields. It returns a map from the name of the selected variants to the corresponding selections. - /// - /// Importantly, it will "flatten" the fragments and handle multiple selections of the same variant. - /// - /// The `context` argument is required so we can expand the fragments. - pub(crate) fn selected_variants_on_union<'s>( - &'s self, - context: &'s crate::query::QueryContext<'_, '_>, - // the name of the type the selection applies to - selection_on: &str, - ) -> Result>, failure::Error> { - let mut selected_variants = BTreeMap::new(); - - self.selected_variants_on_union_inner(context, &mut selected_variants, selection_on)?; - - Ok(selected_variants) - } - - #[cfg(test)] - pub(crate) fn new_empty() -> Selection<'static> { - Selection(Vec::new()) - } - - #[cfg(test)] - pub(crate) fn from_vec(vec: Vec>) -> Self { - Selection(vec) - } - - pub(crate) fn contains_fragment(&self, fragment_name: &str) -> bool { - (&self).into_iter().any(|item| match item { - SelectionItem::Field(field) => field.fields.contains_fragment(fragment_name), - SelectionItem::InlineFragment(inline_fragment) => { - inline_fragment.fields.contains_fragment(fragment_name) - } - SelectionItem::FragmentSpread(fragment) => fragment.fragment_name == fragment_name, - }) - } - - pub(crate) fn len(&self) -> usize { - self.0.len() - } -} - -impl<'query> std::convert::From<&'query SelectionSet> for Selection<'query> { - fn from(selection_set: &SelectionSet) -> Selection<'_> { - use graphql_parser::query::Selection; - - let mut items = Vec::with_capacity(selection_set.items.len()); - - for item in &selection_set.items { - let converted = match item { - Selection::Field(f) => SelectionItem::Field(SelectionField { - alias: f.alias.as_ref().map(String::as_str), - name: &f.name, - fields: (&f.selection_set).into(), - }), - Selection::FragmentSpread(spread) => { - SelectionItem::FragmentSpread(SelectionFragmentSpread { - fragment_name: &spread.fragment_name, - }) - } - Selection::InlineFragment(inline) => { - let graphql_parser::query::TypeCondition::On(ref name) = inline - .type_condition - .as_ref() - .expect("Missing `on` clause."); - SelectionItem::InlineFragment(SelectionInlineFragment { - on: &name, - fields: (&inline.selection_set).into(), - }) - } - }; - items.push(converted); - } - - Selection(items) - } -} - -impl<'a, 'query> std::iter::IntoIterator for &'a Selection<'query> { - type Item = &'a SelectionItem<'query>; - type IntoIter = std::slice::Iter<'a, SelectionItem<'query>>; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter() - } -} - -impl<'a> std::iter::FromIterator> for Selection<'a> { - fn from_iter>>(iter: T) -> Selection<'a> { - Selection(iter.into_iter().collect()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn selection_extract_typename_simple_case() { - let selection = Selection::new_empty(); - let schema = crate::schema::Schema::new(); - let context = crate::query::QueryContext::new_empty(&schema); - - assert!(selection.extract_typename(&context).is_none()); - } - - #[test] - fn selection_extract_typename_in_fragment() { - let mut selection = Selection::new_empty(); - selection - .0 - .push(SelectionItem::FragmentSpread(SelectionFragmentSpread { - fragment_name: "MyFragment", - })); - - let mut fragment_selection = Selection::new_empty(); - fragment_selection - .0 - .push(SelectionItem::Field(SelectionField { - alias: None, - name: "__typename", - fields: Selection::new_empty(), - })); - - let schema = crate::schema::Schema::new(); - let obj = crate::objects::GqlObject::new("MyObject", None); - let mut context = crate::query::QueryContext::new_empty(&schema); - context.fragments.insert( - "MyFragment", - crate::fragments::GqlFragment { - name: "MyFragment", - on: crate::fragments::FragmentTarget::Object(&obj), - selection: fragment_selection, - is_required: std::cell::Cell::new(false), - }, - ); - - assert!(selection.extract_typename(&context).is_some()); - } - - #[test] - fn selection_from_graphql_parser_selection_set() { - let query = r##" - query { - animal { - isCat - isHorse - ...Timestamps - barks - ...on Dog { - rating - } - pawsCount - aliased: sillyName - } - } - "##; - let parsed = graphql_parser::parse_query(query).unwrap(); - let selection_set: &graphql_parser::query::SelectionSet = parsed - .definitions - .iter() - .filter_map(|def| { - if let graphql_parser::query::Definition::Operation( - graphql_parser::query::OperationDefinition::Query(q), - ) = def - { - Some(&q.selection_set) - } else { - None - } - }) - .next() - .unwrap(); - - let selection: Selection<'_> = selection_set.into(); - - assert_eq!( - selection, - Selection(vec![SelectionItem::Field(SelectionField { - alias: None, - name: "animal", - fields: Selection(vec![ - SelectionItem::Field(SelectionField { - alias: None, - name: "isCat", - fields: Selection(Vec::new()), - }), - SelectionItem::Field(SelectionField { - alias: None, - name: "isHorse", - fields: Selection(Vec::new()), - }), - SelectionItem::FragmentSpread(SelectionFragmentSpread { - fragment_name: "Timestamps", - }), - SelectionItem::Field(SelectionField { - alias: None, - name: "barks", - fields: Selection(Vec::new()), - }), - SelectionItem::InlineFragment(SelectionInlineFragment { - on: "Dog", - fields: Selection(vec![SelectionItem::Field(SelectionField { - alias: None, - name: "rating", - fields: Selection(Vec::new()), - })]), - }), - SelectionItem::Field(SelectionField { - alias: None, - name: "pawsCount", - fields: Selection(Vec::new()), - }), - SelectionItem::Field(SelectionField { - alias: Some("aliased"), - name: "sillyName", - fields: Selection(Vec::new()), - }), - ]), - })]) - ); - } -} diff --git a/graphql_client_codegen/src/shared.rs b/graphql_client_codegen/src/shared.rs deleted file mode 100644 index da16e4283..000000000 --- a/graphql_client_codegen/src/shared.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::deprecation::{DeprecationStatus, DeprecationStrategy}; -use crate::objects::GqlObjectField; -use crate::query::QueryContext; -use crate::selection::*; -use failure::*; -use heck::{CamelCase, SnakeCase}; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; - -pub(crate) fn render_object_field( - field_name: &str, - field_type: &TokenStream, - description: Option<&str>, - status: &DeprecationStatus, - strategy: &DeprecationStrategy, -) -> Option { - #[allow(unused_assignments)] - let mut deprecation = quote!(); - match (status, strategy) { - // If the field is deprecated and we are denying usage, don't generate the - // field in rust at all and short-circuit. - (DeprecationStatus::Deprecated(_), DeprecationStrategy::Deny) => return None, - // Everything is allowed so there is nothing to do. - (_, DeprecationStrategy::Allow) => deprecation = quote!(), - // Current so there is nothing to do. - (DeprecationStatus::Current, _) => deprecation = quote!(), - // A reason was provided, translate it to a note. - (DeprecationStatus::Deprecated(Some(reason)), DeprecationStrategy::Warn) => { - deprecation = quote!(#[deprecated(note = #reason)]) - } - // No reason provided, just mark as deprecated. - (DeprecationStatus::Deprecated(None), DeprecationStrategy::Warn) => { - deprecation = quote!(#[deprecated]) - } - }; - - let description = description.map(|s| quote!(#[doc = #s])); - - // List of keywords based on https://doc.rust-lang.org/grammar.html#keywords - let reserved = &[ - "abstract", "alignof", "as", "become", "box", "break", "const", "continue", "crate", "do", - "else", "enum", "extern", "false", "final", "fn", "for", "if", "impl", "in", "let", "loop", - "macro", "match", "mod", "move", "mut", "offsetof", "override", "priv", "proc", "pub", - "pure", "ref", "return", "Self", "self", "sizeof", "static", "struct", "super", "trait", - "true", "type", "typeof", "unsafe", "unsized", "use", "virtual", "where", "while", "yield", - ]; - - if reserved.contains(&field_name) { - let name_ident = Ident::new(&format!("{}_", field_name), Span::call_site()); - return Some(quote! { - #description - #deprecation - #[serde(rename = #field_name)] - pub #name_ident: #field_type - }); - } - - let snake_case_name = field_name.to_snake_case(); - let rename = crate::shared::field_rename_annotation(&field_name, &snake_case_name); - let name_ident = Ident::new(&snake_case_name, Span::call_site()); - - Some(quote!(#description #deprecation #rename pub #name_ident: #field_type)) -} - -pub(crate) fn field_impls_for_selection( - fields: &[GqlObjectField<'_>], - context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, -) -> Result, failure::Error> { - (&selection) - .into_iter() - .map(|selected| { - if let SelectionItem::Field(selected) = selected { - let name = &selected.name; - let alias = selected.alias.as_ref().unwrap_or(name); - - let ty = fields - .iter() - .find(|f| &f.name == name) - .ok_or_else(|| format_err!("could not find field `{}`", name))? - .type_ - .inner_name_str(); - let prefix = format!("{}{}", prefix.to_camel_case(), alias.to_camel_case()); - context.maybe_expand_field(&ty, &selected.fields, &prefix) - } else { - Ok(None) - } - }) - .filter_map(|i| i.transpose()) - .collect() -} - -pub(crate) fn response_fields_for_selection( - type_name: &str, - schema_fields: &[GqlObjectField<'_>], - context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, -) -> Result, failure::Error> { - (&selection) - .into_iter() - .map(|item| match item { - SelectionItem::Field(f) => { - let name = &f.name; - let alias = f.alias.as_ref().unwrap_or(name); - - let schema_field = &schema_fields - .iter() - .find(|field| &field.name == name) - .ok_or_else(|| { - format_err!( - "Could not find field `{}` on `{}`. Available fields: `{}`.", - *name, - type_name, - schema_fields - .iter() - .map(|ref field| &field.name) - .fold(String::new(), |mut acc, item| { - acc.push_str(item); - acc.push_str(", "); - acc - }) - .trim_end_matches(", ") - ) - })?; - let ty = schema_field.type_.to_rust( - context, - &format!("{}{}", prefix.to_camel_case(), alias.to_camel_case()), - ); - - Ok(render_object_field( - alias, - &ty, - schema_field.description.as_ref().cloned(), - &schema_field.deprecation, - &context.deprecation_strategy, - )) - } - SelectionItem::FragmentSpread(fragment) => { - let field_name = - Ident::new(&fragment.fragment_name.to_snake_case(), Span::call_site()); - context.require_fragment(&fragment.fragment_name); - let fragment_from_context = context - .fragments - .get(&fragment.fragment_name) - .ok_or_else(|| format_err!("Unknown fragment: {}", &fragment.fragment_name))?; - let type_name = Ident::new(&fragment.fragment_name, Span::call_site()); - let type_name = if fragment_from_context.is_recursive() { - quote!(Box<#type_name>) - } else { - quote!(#type_name) - }; - Ok(Some(quote! { - #[serde(flatten)] - pub #field_name: #type_name - })) - } - SelectionItem::InlineFragment(_) => Err(format_err!( - "unimplemented: inline fragment on object field" - ))?, - }) - .filter_map(|x| match x { - // Remove empty fields so callers always know a field has some - // tokens. - Ok(f) => f.map(Ok), - Err(err) => Some(Err(err)), - }) - .collect() -} - -/// Given the GraphQL schema name for an object/interface/input object field and -/// the equivalent rust name, produces a serde annotation to map them during -/// (de)serialization if it is necessary, otherwise an empty TokenStream. -pub(crate) fn field_rename_annotation(graphql_name: &str, rust_name: &str) -> Option { - if graphql_name != rust_name { - Some(quote!(#[serde(rename = #graphql_name)])) - } else { - None - } -} diff --git a/graphql_client_codegen/src/tests/foobars_query.graphql b/graphql_client_codegen/src/tests/foobars_query.graphql new file mode 100644 index 000000000..25471877c --- /dev/null +++ b/graphql_client_codegen/src/tests/foobars_query.graphql @@ -0,0 +1,16 @@ +query FooBarsQuery { + fooBars { + fooBars { + __typename + ... on Foo { + fooField + } + ... on Bar { + barField + } + ... on FooBar { + fooBarField + } + } + } +} diff --git a/graphql_client_codegen/src/tests/foobars_schema.graphql b/graphql_client_codegen/src/tests/foobars_schema.graphql new file mode 100644 index 000000000..f24848959 --- /dev/null +++ b/graphql_client_codegen/src/tests/foobars_schema.graphql @@ -0,0 +1,28 @@ +schema { + query: Query + mutation: Mutation +} + +directive @defer on FIELD + +type Query { + fooBars: Self +} + +type Self { + fooBars: Result +} + +union Result = Foo | Bar | FooBar + +type Foo { + fooField: String! +} + +type Bar { + barField: String! +} + +type FooBar { + fooBarField: String! +} diff --git a/graphql_client_codegen/src/tests/github.rs b/graphql_client_codegen/src/tests/github.rs deleted file mode 100644 index 7a72fa388..000000000 --- a/graphql_client_codegen/src/tests/github.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::schema::Schema; -use std::collections::HashSet; - -const SCHEMA_JSON: &str = include_str!("github_schema.json"); -const SCHEMA_GRAPHQL: &str = include_str!("github_schema.graphql"); - -#[test] -fn ast_from_graphql_and_json_produce_the_same_schema() { - use std::iter::FromIterator; - let json: graphql_introspection_query::introspection_response::IntrospectionResponse = - serde_json::from_str(SCHEMA_JSON).unwrap(); - let graphql_parser_schema = graphql_parser::parse_schema(SCHEMA_GRAPHQL).unwrap(); - let json = Schema::from(&json); - let gql = Schema::from(&graphql_parser_schema); - - assert_eq!(json.scalars, gql.scalars); - for (json, gql) in json.objects.iter().zip(gql.objects.iter()) { - for (j, g) in json.1.fields.iter().zip(gql.1.fields.iter()) { - assert_eq!(j, g); - } - assert_eq!(json, gql) - } - for (json, gql) in json.unions.iter().zip(gql.unions.iter()) { - assert_eq!(json, gql) - } - for (json, gql) in json.interfaces.iter().zip(gql.interfaces.iter()) { - assert_eq!(json, gql) - } - assert_eq!(json.interfaces, gql.interfaces); - assert_eq!(json.query_type, gql.query_type); - assert_eq!(json.mutation_type, gql.mutation_type); - assert_eq!(json.subscription_type, gql.subscription_type); - for (json, gql) in json.inputs.iter().zip(gql.inputs.iter()) { - assert_eq!(json, gql); - } - assert_eq!(json.inputs, gql.inputs, "inputs differ"); - for ((json_name, json_value), (gql_name, gql_value)) in json.enums.iter().zip(gql.enums.iter()) - { - assert_eq!(json_name, gql_name); - assert_eq!( - HashSet::<&str>::from_iter(json_value.variants.iter().map(|v| v.name)), - HashSet::<&str>::from_iter(gql_value.variants.iter().map(|v| v.name)), - ); - } -} diff --git a/graphql_client_codegen/src/tests/keywords_query.graphql b/graphql_client_codegen/src/tests/keywords_query.graphql new file mode 100644 index 000000000..5503ab706 --- /dev/null +++ b/graphql_client_codegen/src/tests/keywords_query.graphql @@ -0,0 +1,8 @@ +query searchQuery($criteria: extern!) { + search { + transactions(criteria: $searchID) { + for + status + } + } +} diff --git a/graphql_client_codegen/src/tests/keywords_schema.graphql b/graphql_client_codegen/src/tests/keywords_schema.graphql new file mode 100644 index 000000000..25cafc7f7 --- /dev/null +++ b/graphql_client_codegen/src/tests/keywords_schema.graphql @@ -0,0 +1,76 @@ +schema { + query: Query + mutation: Mutation +} + +""" +This directive allows results to be deferred during execution +""" +directive @defer on FIELD + +""" +The top-level Query type. +""" +type Query { + """ + Keyword type + """ + search: Self +} + +""" +Keyword type +""" +type Self { + """ + A keyword variable name with a keyword-named input type + """ + transactions(struct: extern!): Result +} + +""" +Keyword type +""" +type Result { + """ + Keyword field. + """ + for: String + """ + dummy field with enum + """ + status: AnEnum +} + +""" +Keyword input +""" +input extern { + """ + A field + """ + id: crate +} + +""" +Input fields for searching for specific values. +""" +input crate { + """ + Keyword field. + """ + enum: String + + """ + Keyword field. + """ + in: [String!] +} + +""" +Enum with keywords +""" +enum AnEnum { + where + self +} diff --git a/graphql_client_codegen/src/tests/mod.rs b/graphql_client_codegen/src/tests/mod.rs index 6a5a51cbc..aaed3e5d0 100644 --- a/graphql_client_codegen/src/tests/mod.rs +++ b/graphql_client_codegen/src/tests/mod.rs @@ -1 +1,156 @@ -mod github; +use std::path::PathBuf; + +use crate::{generate_module_token_stream_from_string, CodegenMode, GraphQLClientCodegenOptions}; + +const KEYWORDS_QUERY: &str = include_str!("keywords_query.graphql"); +const KEYWORDS_SCHEMA_PATH: &str = "keywords_schema.graphql"; + +const FOOBARS_QUERY: &str = include_str!("foobars_query.graphql"); +const FOOBARS_SCHEMA_PATH: &str = "foobars_schema.graphql"; + +fn build_schema_path(path: &str) -> PathBuf { + std::env::current_dir() + .unwrap() + .join("src/tests") + .join(path) +} + +#[test] +fn schema_with_keywords_works() { + let query_string = KEYWORDS_QUERY; + let schema_path = build_schema_path(KEYWORDS_SCHEMA_PATH); + + let options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); + + let generated_tokens = + generate_module_token_stream_from_string(query_string, &schema_path, options) + .expect("Generate keywords module"); + + let generated_code = generated_tokens.to_string(); + + // Parse generated code. All keywords should be correctly escaped. + let r: syn::parse::Result = syn::parse2(generated_tokens); + match r { + Ok(_) => { + // Rust keywords should be escaped / renamed now + assert!(generated_code.contains("pub in_")); + assert!(generated_code.contains("extern_")); + } + Err(e) => { + panic!("Error: {}\n Generated content: {}\n", e, &generated_code); + } + }; +} + +#[test] +fn blended_custom_types_works() { + let query_string = KEYWORDS_QUERY; + let schema_path = build_schema_path(KEYWORDS_SCHEMA_PATH); + + let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); + options.set_custom_response_type("external_crate::Transaction".to_string()); + options.set_custom_variable_types(vec!["external_crate::ID".to_string()]); + + let generated_tokens = + generate_module_token_stream_from_string(query_string, &schema_path, options) + .expect("Generate keywords module"); + + let generated_code = generated_tokens.to_string(); + + // Parse generated code. Variables and returns should be replaced with custom types + let r: syn::parse::Result = syn::parse2(generated_tokens); + match r { + Ok(_) => { + // Variables and returns should be replaced with custom types + assert!(generated_code.contains("pub type SearchQuerySearch = external_crate :: Transaction")); + assert!(generated_code.contains("pub type extern_ = external_crate :: ID")); + } + Err(e) => { + panic!("Error: {}\n Generated content: {}\n", e, &generated_code); + } + }; +} + +#[test] +fn fragments_other_variant_should_generate_unknown_other_variant() { + let query_string = FOOBARS_QUERY; + let schema_path = build_schema_path(FOOBARS_SCHEMA_PATH); + + let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); + + options.set_fragments_other_variant(true); + + let generated_tokens = + generate_module_token_stream_from_string(query_string, &schema_path, options) + .expect("Generate foobars module"); + + let generated_code = generated_tokens.to_string(); + + let r: syn::parse::Result = syn::parse2(generated_tokens); + match r { + Ok(_) => { + // Rust keywords should be escaped / renamed now + assert!(generated_code.contains("# [serde (other)] Unknown")); + assert!(generated_code.contains("Unknown")); + } + Err(e) => { + panic!("Error: {}\n Generated content: {}\n", e, &generated_code); + } + }; +} + +#[test] +fn fragments_other_variant_false_should_not_generate_unknown_other_variant() { + let query_string = FOOBARS_QUERY; + let schema_path = build_schema_path(FOOBARS_SCHEMA_PATH); + + let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); + + options.set_fragments_other_variant(false); + + let generated_tokens = + generate_module_token_stream_from_string(query_string, &schema_path, options) + .expect("Generate foobars module token stream"); + + let generated_code = generated_tokens.to_string(); + + let r: syn::parse::Result = syn::parse2(generated_tokens); + match r { + Ok(_) => { + // Rust keywords should be escaped / renamed now + assert!(!generated_code.contains("# [serde (other)] Unknown")); + assert!(!generated_code.contains("Unknown")); + } + Err(e) => { + panic!("Error: {}\n Generated content: {}\n", e, &generated_code); + } + }; +} + +#[test] +fn skip_serializing_none_should_generate_serde_skip_serializing() { + let query_string = KEYWORDS_QUERY; + let schema_path = build_schema_path(KEYWORDS_SCHEMA_PATH); + + let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); + + options.set_skip_serializing_none(true); + + let generated_tokens = + generate_module_token_stream_from_string(query_string, &schema_path, options) + .expect("Generate foobars module"); + + let generated_code = generated_tokens.to_string(); + + let r: syn::parse::Result = syn::parse2(generated_tokens); + + match r { + Ok(_) => { + println!("{}", generated_code); + assert!(generated_code.contains("skip_serializing_if")); + } + Err(e) => { + panic!("Error: {}\n Generated content: {}\n", e, &generated_code); + } + }; +} diff --git a/graphql_client_codegen/src/type_qualifiers.rs b/graphql_client_codegen/src/type_qualifiers.rs new file mode 100644 index 000000000..972fdb54e --- /dev/null +++ b/graphql_client_codegen/src/type_qualifiers.rs @@ -0,0 +1,22 @@ +#[derive(Clone, Debug, PartialEq, Hash)] +pub(crate) enum GraphqlTypeQualifier { + Required, + List, +} + +impl GraphqlTypeQualifier { + pub(crate) fn is_required(&self) -> bool { + *self == GraphqlTypeQualifier::Required + } +} + +pub fn graphql_parser_depth<'doc, T>(schema_type: &graphql_parser::schema::Type<'doc, T>) -> usize +where + T: graphql_parser::query::Text<'doc>, +{ + match schema_type { + graphql_parser::schema::Type::ListType(inner) => 1 + graphql_parser_depth(inner), + graphql_parser::schema::Type::NonNullType(inner) => 1 + graphql_parser_depth(inner), + graphql_parser::schema::Type::NamedType(_) => 0, + } +} diff --git a/graphql_client_codegen/src/unions.rs b/graphql_client_codegen/src/unions.rs deleted file mode 100644 index c3b837484..000000000 --- a/graphql_client_codegen/src/unions.rs +++ /dev/null @@ -1,362 +0,0 @@ -use crate::query::QueryContext; -use crate::selection::Selection; -use failure::*; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use std::cell::Cell; -use std::collections::BTreeSet; - -/// A GraphQL union (simplified schema representation). -/// -/// For code generation purposes, unions will "flatten" fragment spreads, so there is only one enum for the selection. See the tests in the graphql_client crate for examples. -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct GqlUnion<'schema> { - pub name: &'schema str, - pub description: Option<&'schema str>, - pub variants: BTreeSet<&'schema str>, - pub is_required: Cell, -} - -#[derive(Debug, Fail)] -#[fail(display = "UnionError")] -enum UnionError { - #[fail(display = "Unknown type: {}", ty)] - UnknownType { ty: String }, - #[fail(display = "Missing __typename in selection for {}", union_name)] - MissingTypename { union_name: String }, -} - -type UnionVariantResult<'selection> = - Result<(Vec, Vec, Vec<&'selection str>), failure::Error>; - -/// Returns a triple. -/// -/// - The first element is the union variants to be inserted directly into the `enum` declaration. -/// - The second is the structs for each variant's sub-selection -/// - The last one contains which fields have been selected on the union, so we can make the enum exhaustive by complementing with those missing. -pub(crate) fn union_variants<'selection>( - selection: &'selection Selection<'_>, - context: &'selection QueryContext<'selection, 'selection>, - prefix: &str, - selection_on: &str, -) -> UnionVariantResult<'selection> { - let selection = selection.selected_variants_on_union(context, selection_on)?; - let mut used_variants: Vec<&str> = selection.keys().cloned().collect(); - let mut children_definitions = Vec::with_capacity(selection.len()); - let mut variants = Vec::with_capacity(selection.len()); - - for (on, fields) in selection.iter() { - let variant_name = Ident::new(&on, Span::call_site()); - used_variants.push(on); - - let new_prefix = format!("{}On{}", prefix, on); - - let variant_type = Ident::new(&new_prefix, Span::call_site()); - - let field_object_type = context - .schema - .objects - .get(on) - .map(|_f| context.maybe_expand_field(&on, fields, &new_prefix)); - let field_interface = context - .schema - .interfaces - .get(on) - .map(|_f| context.maybe_expand_field(&on, fields, &new_prefix)); - let field_union_type = context - .schema - .unions - .get(on) - .map(|_f| context.maybe_expand_field(&on, fields, &new_prefix)); - - match field_object_type.or(field_interface).or(field_union_type) { - Some(Ok(Some(tokens))) => children_definitions.push(tokens), - Some(Err(err)) => Err(err)?, - Some(Ok(None)) => (), - None => Err(UnionError::UnknownType { ty: on.to_string() })?, - }; - - variants.push(quote! { - #variant_name(#variant_type) - }) - } - - Ok((variants, children_definitions, used_variants)) -} - -impl<'schema> GqlUnion<'schema> { - /// Returns the code to deserialize this union in the response given the query selection. - pub(crate) fn response_for_selection( - &self, - query_context: &QueryContext<'_, '_>, - selection: &Selection<'_>, - prefix: &str, - ) -> Result { - let typename_field = selection.extract_typename(query_context); - - if typename_field.is_none() { - Err(UnionError::MissingTypename { - union_name: prefix.into(), - })?; - } - - let struct_name = Ident::new(prefix, Span::call_site()); - let derives = query_context.response_derives(); - - let (mut variants, children_definitions, used_variants) = - union_variants(selection, query_context, prefix, &self.name)?; - - variants.extend( - self.variants - .iter() - .filter(|v| used_variants.iter().find(|a| a == v).is_none()) - .map(|v| { - let v = Ident::new(v, Span::call_site()); - quote!(#v) - }), - ); - - Ok(quote! { - #(#children_definitions)* - - #derives - #[serde(tag = "__typename")] - pub enum #struct_name { - #(#variants),* - } - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::constants::*; - use crate::deprecation::DeprecationStatus; - use crate::field_type::FieldType; - use crate::objects::{GqlObject, GqlObjectField}; - use crate::selection::*; - - #[test] - fn union_response_for_selection_complains_if_typename_is_missing() { - let fields = vec![ - SelectionItem::InlineFragment(SelectionInlineFragment { - on: "User", - fields: Selection::from_vec(vec![SelectionItem::Field(SelectionField { - alias: None, - name: "firstName", - fields: Selection::new_empty(), - })]), - }), - SelectionItem::InlineFragment(SelectionInlineFragment { - on: "Organization", - fields: Selection::from_vec(vec![SelectionItem::Field(SelectionField { - alias: None, - name: "title", - fields: Selection::new_empty(), - })]), - }), - ]; - let selection = Selection::from_vec(fields); - let prefix = "Meow"; - let union = GqlUnion { - name: "MyUnion", - description: None, - variants: BTreeSet::new(), - is_required: false.into(), - }; - - let mut schema = crate::schema::Schema::new(); - - schema.objects.insert( - "User", - GqlObject { - description: None, - name: "User", - fields: vec![ - GqlObjectField { - description: None, - name: "firstName", - type_: FieldType::new("String").nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "lastName", - type_: FieldType::new("String").nonnull(), - - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "createdAt", - type_: FieldType::new("Date").nonnull(), - deprecation: DeprecationStatus::Current, - }, - ], - is_required: false.into(), - }, - ); - - schema.objects.insert( - "Organization", - GqlObject { - description: None, - name: "Organization", - fields: vec![ - GqlObjectField { - description: None, - name: "title", - type_: FieldType::new("String").nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "created_at", - type_: FieldType::new("Date").nonnull(), - deprecation: DeprecationStatus::Current, - }, - ], - is_required: false.into(), - }, - ); - let context = QueryContext::new_empty(&schema); - - let result = union.response_for_selection(&context, &selection, &prefix); - - assert!(result.is_err()); - - assert_eq!( - format!("{}", result.unwrap_err()), - "Missing __typename in selection for Meow" - ); - } - - #[test] - fn union_response_for_selection_works() { - let fields = vec![ - SelectionItem::Field(SelectionField { - alias: None, - name: "__typename", - fields: Selection::new_empty(), - }), - SelectionItem::InlineFragment(SelectionInlineFragment { - on: "User", - fields: Selection::from_vec(vec![SelectionItem::Field(SelectionField { - alias: None, - name: "firstName", - fields: Selection::new_empty(), - })]), - }), - SelectionItem::InlineFragment(SelectionInlineFragment { - on: "Organization", - fields: Selection::from_vec(vec![SelectionItem::Field(SelectionField { - alias: None, - name: "title", - fields: Selection::new_empty(), - })]), - }), - ]; - let schema = crate::schema::Schema::new(); - let context = QueryContext::new_empty(&schema); - let selection: Selection<'_> = fields.into_iter().collect(); - let prefix = "Meow"; - let union = GqlUnion { - name: "MyUnion", - description: None, - variants: BTreeSet::new(), - is_required: false.into(), - }; - - let result = union.response_for_selection(&context, &selection, &prefix); - - assert!(result.is_err()); - - let mut schema = crate::schema::Schema::new(); - schema.objects.insert( - "User", - GqlObject { - description: None, - name: "User", - fields: vec![ - GqlObjectField { - description: None, - name: "__typename", - type_: FieldType::new(string_type()).nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "firstName", - type_: FieldType::new(string_type()).nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "lastName", - type_: FieldType::new(string_type()).nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "createdAt", - type_: FieldType::new("Date").nonnull(), - deprecation: DeprecationStatus::Current, - }, - ], - is_required: false.into(), - }, - ); - - schema.objects.insert( - "Organization", - GqlObject { - description: None, - name: "Organization", - fields: vec![ - GqlObjectField { - description: None, - name: "__typename", - type_: FieldType::new(string_type()).nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "title", - type_: FieldType::new("String").nonnull(), - deprecation: DeprecationStatus::Current, - }, - GqlObjectField { - description: None, - name: "createdAt", - type_: FieldType::new("Date").nonnull(), - deprecation: DeprecationStatus::Current, - }, - ], - is_required: false.into(), - }, - ); - - let context = QueryContext::new_empty(&schema); - - let result = union.response_for_selection(&context, &selection, &prefix); - - println!("{:?}", result); - - assert!(result.is_ok()); - - assert_eq!( - result.unwrap().to_string(), - vec![ - "# [ derive ( Deserialize ) ] ", - "pub struct MeowOnOrganization { pub title : String , } ", - "# [ derive ( Deserialize ) ] ", - "pub struct MeowOnUser { # [ serde ( rename = \"firstName\" ) ] pub first_name : String , } ", - "# [ derive ( Deserialize ) ] ", - "# [ serde ( tag = \"__typename\" ) ] ", - "pub enum Meow { Organization ( MeowOnOrganization ) , User ( MeowOnUser ) }", - ].into_iter() - .collect::(), - ); - } -} diff --git a/graphql_client_codegen/src/variables.rs b/graphql_client_codegen/src/variables.rs deleted file mode 100644 index 7865f6e28..000000000 --- a/graphql_client_codegen/src/variables.rs +++ /dev/null @@ -1,135 +0,0 @@ -use crate::field_type::FieldType; -use crate::query::QueryContext; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; -use std::collections::BTreeMap; - -#[derive(Debug, Clone)] -pub struct Variable<'query> { - pub name: &'query str, - pub ty: FieldType<'query>, - pub default: Option<&'query graphql_parser::query::Value>, -} - -impl<'query> Variable<'query> { - pub(crate) fn generate_default_value_constructor( - &self, - context: &QueryContext<'_, '_>, - ) -> Option { - context.schema.require(&self.ty.inner_name_str()); - match &self.default { - Some(default) => { - let fn_name = Ident::new(&format!("default_{}", self.name), Span::call_site()); - let ty = self.ty.to_rust(context, ""); - let value = graphql_parser_value_to_literal( - default, - context, - &self.ty, - self.ty.is_optional(), - ); - Some(quote! { - pub fn #fn_name() -> #ty { - #value - } - - }) - } - None => None, - } - } -} - -impl<'query> std::convert::From<&'query graphql_parser::query::VariableDefinition> - for Variable<'query> -{ - fn from(def: &'query graphql_parser::query::VariableDefinition) -> Variable<'query> { - Variable { - name: &def.name, - ty: FieldType::from(&def.var_type), - default: def.default_value.as_ref(), - } - } -} - -fn graphql_parser_value_to_literal( - value: &graphql_parser::query::Value, - context: &QueryContext<'_, '_>, - ty: &FieldType<'_>, - is_optional: bool, -) -> TokenStream { - use graphql_parser::query::Value; - - let inner = match value { - Value::Boolean(b) => { - if *b { - quote!(true) - } else { - quote!(false) - } - } - Value::String(s) => quote!(#s.to_string()), - Value::Variable(_) => panic!("variable in variable"), - Value::Null => panic!("null as default value"), - Value::Float(f) => quote!(#f), - Value::Int(i) => { - let i = i.as_i64(); - quote!(#i) - } - Value::Enum(en) => quote!(#en), - Value::List(inner) => { - let elements = inner - .iter() - .map(|val| graphql_parser_value_to_literal(val, context, ty, false)); - quote! { - vec![ - #(#elements,)* - ] - } - } - Value::Object(obj) => render_object_literal(obj, ty, context), - }; - - if is_optional { - quote!(Some(#inner)) - } else { - inner - } -} - -fn render_object_literal( - object: &BTreeMap, - ty: &FieldType<'_>, - context: &QueryContext<'_, '_>, -) -> TokenStream { - let type_name = ty.inner_name_str(); - let constructor = Ident::new(&type_name, Span::call_site()); - let schema_type = context - .schema - .inputs - .get(type_name) - .expect("unknown input type"); - let fields: Vec = schema_type - .fields - .iter() - .map(|(name, field)| { - let field_name = Ident::new(&name, Span::call_site()); - let provided_value = object.get(name.to_owned()); - match provided_value { - Some(default_value) => { - let value = graphql_parser_value_to_literal( - default_value, - context, - &field.type_, - field.type_.is_optional(), - ); - quote!(#field_name: #value) - } - None => quote!(#field_name: None), - } - }) - .collect(); - - quote!(#constructor { - #(#fields,)* - }) -} diff --git a/graphql_client_web/.gitignore b/graphql_client_web/.gitignore deleted file mode 100644 index f32d710ca..000000000 --- a/graphql_client_web/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/wasm-pack.log -/bin diff --git a/graphql_client_web/Cargo.toml b/graphql_client_web/Cargo.toml deleted file mode 100644 index 05a07ea91..000000000 --- a/graphql_client_web/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "graphql_client_web" -version = "0.8.0" -authors = ["Tom Houlé "] -edition = "2018" -description = "Typed GraphQL requests and responses (web integration)" -license = "Apache-2.0 OR MIT" -keywords = ["graphql", "api", "web", "webassembly", "wasm"] -categories = ["network-programming", "web-programming", "wasm"] -repository = "https://github.com/graphql-rust/graphql-client" - -[dependencies.graphql_client] -version = "0.8.0" -path = "../graphql_client" -features = ["web"] - -[dev-dependencies] -serde = { version = "^1.0", features = ["derive"] } -wasm-bindgen-test = "0.2.50" diff --git a/graphql_client_web/README.md b/graphql_client_web/README.md deleted file mode 100644 index 6006bd4af..000000000 --- a/graphql_client_web/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# graphql_client_web - -Make boilerplate-free GraphQL API calls from web browsers using [graphql-client](../README.md) and [wasm-bindgen](https://github.com/alexcrichton/wasm-bindgen). - -For usage details, see the [API docs](https://docs.rs/graphql_client_web/latest), the [example](../graphql_client/examples/web) and the [tests](./tests/web.rs). diff --git a/graphql_client_web/src/lib.rs b/graphql_client_web/src/lib.rs deleted file mode 100644 index 02a066d73..000000000 --- a/graphql_client_web/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -#![deprecated( - note = "graphql_client_web is deprecated. The web client is now part of the graphql_client crate, with the \"web\" feature." -)] - -pub use graphql_client::web::*; -pub use graphql_client::{self, *}; diff --git a/graphql_query_derive/.gitignore b/graphql_query_derive/.gitignore deleted file mode 100644 index ea8c4bf7f..000000000 --- a/graphql_query_derive/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/graphql_query_derive/Cargo.toml b/graphql_query_derive/Cargo.toml index 5ad2b754f..b48f58459 100644 --- a/graphql_query_derive/Cargo.toml +++ b/graphql_query_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graphql_query_derive" -version = "0.8.0" +version = "0.14.0" authors = ["Tom Houlé "] description = "Utility crate for graphql_client" license = "Apache-2.0 OR MIT" @@ -11,7 +11,6 @@ edition = "2018" proc-macro = true [dependencies] -failure = "^0.1" -syn = { version = "^1.0", features = ["extra-traits"] } +syn = { version = "^2.0", features = ["extra-traits"] } proc-macro2 = { version = "^1.0", features = [] } -graphql_client_codegen = { path = "../graphql_client_codegen/", version = "0.8.0" } +graphql_client_codegen = { path = "../graphql_client_codegen/", version = "0.14.0" } diff --git a/graphql_query_derive/src/attributes.rs b/graphql_query_derive/src/attributes.rs index 0bd4ade59..535914fbb 100644 --- a/graphql_query_derive/src/attributes.rs +++ b/graphql_query_derive/src/attributes.rs @@ -1,49 +1,133 @@ -use failure::*; +use proc_macro2::TokenTree; +use std::str::FromStr; +use syn::Meta; + use graphql_client_codegen::deprecation::DeprecationStrategy; -use syn; +use graphql_client_codegen::normalization::Normalization; const DEPRECATION_ERROR: &str = "deprecated must be one of 'allow', 'deny', or 'warn'"; +const NORMALIZATION_ERROR: &str = "normalization must be one of 'none' or 'rust'"; + +pub fn ident_exists(ast: &syn::DeriveInput, ident: &str) -> Result<(), syn::Error> { + let attribute = ast + .attrs + .iter() + .find(|attr| attr.path().is_ident("graphql")) + .ok_or_else(|| syn::Error::new_spanned(ast, "The graphql attribute is missing"))?; + + if let Meta::List(list) = &attribute.meta { + for item in list.tokens.clone().into_iter() { + if let TokenTree::Ident(ident_) = item { + if ident_ == ident { + return Ok(()); + } + } + } + } -/// The `graphql` attribute as a `syn::Path`. -fn path_to_match() -> syn::Path { - syn::parse_str("graphql").expect("`graphql` is a valid path") + Err(syn::Error::new_spanned( + ast, + format!("Ident `{}` not found", ident), + )) } /// Extract an configuration parameter specified in the `graphql` attribute. -pub fn extract_attr(ast: &syn::DeriveInput, attr: &str) -> Result { - let attributes = &ast.attrs; - let graphql_path = path_to_match(); - let attribute = attributes +pub fn extract_attr(ast: &syn::DeriveInput, attr: &str) -> Result { + let attribute = ast + .attrs .iter() - .find(|attr| attr.path == graphql_path) - .ok_or_else(|| format_err!("The graphql attribute is missing"))?; - if let syn::Meta::List(items) = &attribute.parse_meta().expect("Attribute is well formatted") { - for item in items.nested.iter() { - if let syn::NestedMeta::Meta(syn::Meta::NameValue(name_value)) = item { - let syn::MetaNameValue { path, lit, .. } = name_value; - if let Some(ident) = path.get_ident() { - if ident == attr { - if let syn::Lit::Str(lit) = lit { - return Ok(lit.value()); + .find(|a| a.path().is_ident("graphql")) + .ok_or_else(|| syn::Error::new_spanned(ast, "The graphql attribute is missing"))?; + + if let Meta::List(list) = &attribute.meta { + let mut iter = list.tokens.clone().into_iter(); + while let Some(item) = iter.next() { + if let TokenTree::Ident(ident) = item { + if ident == attr { + iter.next(); + if let Some(TokenTree::Literal(lit)) = iter.next() { + let lit_str: syn::LitStr = syn::parse_str(&lit.to_string())?; + return Ok(lit_str.value()); + } + } + } + } + } + + Err(syn::Error::new_spanned( + ast, + format!("Attribute `{}` not found", attr), + )) +} + +/// Extract a list of configuration parameter values specified in the `graphql` attribute. +pub fn extract_attr_list(ast: &syn::DeriveInput, attr: &str) -> Result, syn::Error> { + let attribute = ast + .attrs + .iter() + .find(|a| a.path().is_ident("graphql")) + .ok_or_else(|| syn::Error::new_spanned(ast, "The graphql attribute is missing"))?; + + let mut result = Vec::new(); + + if let Meta::List(list) = &attribute.meta { + let mut iter = list.tokens.clone().into_iter(); + while let Some(item) = iter.next() { + if let TokenTree::Ident(ident) = item { + if ident == attr { + if let Some(TokenTree::Group(group)) = iter.next() { + for token in group.stream() { + if let TokenTree::Literal(lit) = token { + let lit_str: syn::LitStr = syn::parse_str(&lit.to_string())?; + result.push(lit_str.value()); + } } + return Ok(result); } } } } } - Err(format_err!("attribute not found"))? + if result.is_empty() { + Err(syn::Error::new_spanned( + ast, + format!("Attribute list `{}` not found or empty", attr), + )) + } else { + Ok(result) + } } /// Get the deprecation from a struct attribute in the derive case. pub fn extract_deprecation_strategy( ast: &syn::DeriveInput, -) -> Result { - extract_attr(&ast, "deprecated")? +) -> Result { + extract_attr(ast, "deprecated")? + .to_lowercase() + .as_str() + .parse() + .map_err(|_| syn::Error::new_spanned(ast, DEPRECATION_ERROR.to_owned())) +} + +/// Get the deprecation from a struct attribute in the derive case. +pub fn extract_normalization(ast: &syn::DeriveInput) -> Result { + extract_attr(ast, "normalization")? .to_lowercase() .as_str() .parse() - .map_err(|_| format_err!("{}", DEPRECATION_ERROR)) + .map_err(|_| syn::Error::new_spanned(ast, NORMALIZATION_ERROR)) +} + +pub fn extract_fragments_other_variant(ast: &syn::DeriveInput) -> bool { + extract_attr(ast, "fragments_other_variant") + .ok() + .and_then(|s| FromStr::from_str(s.as_str()).ok()) + .unwrap_or(false) +} + +pub fn extract_skip_serializing_none(ast: &syn::DeriveInput) -> bool { + ident_exists(ast, "skip_serializing_none").is_ok() } #[cfg(test)] @@ -103,4 +187,152 @@ mod test { Err(e) => assert_eq!(&format!("{}", e), DEPRECATION_ERROR), }; } + + #[test] + fn test_fragments_other_variant_set_to_true() { + let input = " + #[derive(GraphQLQuery)] + #[graphql( + schema_path = \"x\", + query_path = \"x\", + fragments_other_variant = \"true\", + )] + struct MyQuery; + "; + let parsed = syn::parse_str(input).unwrap(); + assert!(extract_fragments_other_variant(&parsed)); + } + + #[test] + fn test_fragments_other_variant_set_to_false() { + let input = " + #[derive(GraphQLQuery)] + #[graphql( + schema_path = \"x\", + query_path = \"x\", + fragments_other_variant = \"false\", + )] + struct MyQuery; + "; + let parsed = syn::parse_str(input).unwrap(); + assert!(!extract_fragments_other_variant(&parsed)); + } + + #[test] + fn test_fragments_other_variant_set_to_invalid() { + let input = " + #[derive(GraphQLQuery)] + #[graphql( + schema_path = \"x\", + query_path = \"x\", + fragments_other_variant = \"invalid\", + )] + struct MyQuery; + "; + let parsed = syn::parse_str(input).unwrap(); + assert!(!extract_fragments_other_variant(&parsed)); + } + + #[test] + fn test_fragments_other_variant_unset() { + let input = " + #[derive(GraphQLQuery)] + #[graphql( + schema_path = \"x\", + query_path = \"x\", + )] + struct MyQuery; + "; + let parsed = syn::parse_str(input).unwrap(); + assert!(!extract_fragments_other_variant(&parsed)); + } + + #[test] + fn test_skip_serializing_none_set() { + let input = r#" + #[derive(GraphQLQuery)] + #[graphql( + schema_path = "x", + query_path = "x", + skip_serializing_none + )] + struct MyQuery; + "#; + let parsed = syn::parse_str(input).unwrap(); + assert!(extract_skip_serializing_none(&parsed)); + } + + #[test] + fn test_skip_serializing_none_unset() { + let input = r#" + #[derive(GraphQLQuery)] + #[graphql( + schema_path = "x", + query_path = "x", + )] + struct MyQuery; + "#; + let parsed = syn::parse_str(input).unwrap(); + assert!(!extract_skip_serializing_none(&parsed)); + } + + #[test] + fn test_external_enums() { + let input = r#" + #[derive(Serialize, Deserialize, Debug)] + #[derive(GraphQLQuery)] + #[graphql( + schema_path = "x", + query_path = "x", + extern_enums("Direction", "DistanceUnit"), + )] + struct MyQuery; + "#; + let parsed: syn::DeriveInput = syn::parse_str(input).unwrap(); + + assert_eq!( + extract_attr_list(&parsed, "extern_enums").ok().unwrap(), + vec!["Direction", "DistanceUnit"], + ); + } + + #[test] + fn test_custom_variable_types() { + let input = r#" + #[derive(Serialize, Deserialize, Debug)] + #[derive(GraphQLQuery)] + #[graphql( + schema_path = "x", + query_path = "x", + variable_types("extern_crate::Var1", "extern_crate::Var2"), + )] + struct MyQuery; + "#; + let parsed: syn::DeriveInput = syn::parse_str(input).unwrap(); + + assert_eq!( + extract_attr_list(&parsed, "variable_types").ok().unwrap(), + vec!["extern_crate::Var1", "extern_crate::Var2"], + ); + } + + #[test] + fn test_custom_response_type() { + let input = r#" + #[derive(Serialize, Deserialize, Debug)] + #[derive(GraphQLQuery)] + #[graphql( + schema_path = "x", + query_path = "x", + response_type = "extern_crate::Resp", + )] + struct MyQuery; + "#; + let parsed: syn::DeriveInput = syn::parse_str(input).unwrap(); + + assert_eq!( + extract_attr(&parsed, "response_type").ok().unwrap(), + "extern_crate::Resp", + ); + } } diff --git a/graphql_query_derive/src/lib.rs b/graphql_query_derive/src/lib.rs index 1aadae7c2..c6a7eca3a 100644 --- a/graphql_query_derive/src/lib.rs +++ b/graphql_query_derive/src/lib.rs @@ -3,11 +3,13 @@ extern crate proc_macro; /// Derive-related code. This will be moved into graphql_query_derive. mod attributes; -use failure::ResultExt; use graphql_client_codegen::{ generate_module_token_stream, CodegenMode, GraphQLClientCodegenOptions, }; -use std::path::{Path, PathBuf}; +use std::{ + env, + path::{Path, PathBuf}, +}; use proc_macro2::TokenStream; @@ -15,44 +17,40 @@ use proc_macro2::TokenStream; pub fn derive_graphql_query(input: proc_macro::TokenStream) -> proc_macro::TokenStream { match graphql_query_derive_inner(input) { Ok(ts) => ts, - Err(err) => panic!( - "{}", - err.iter_chain() - .fold(String::new(), |mut acc, item| { - acc.push_str(&format!("{}\n", item)); - acc - }) - .trim_end_matches('\n') - ), + Err(err) => err.to_compile_error().into(), } } fn graphql_query_derive_inner( input: proc_macro::TokenStream, -) -> Result { +) -> Result { let input = TokenStream::from(input); - let ast = syn::parse2(input).context("Derive input parsing.")?; + let ast = syn::parse2(input)?; let (query_path, schema_path) = build_query_and_schema_path(&ast)?; - let options = build_graphql_client_derive_options(&ast, query_path.to_path_buf())?; - Ok( - generate_module_token_stream(query_path, &schema_path, options) - .map(Into::into) - .context("Code generation failed.")?, - ) + let options = build_graphql_client_derive_options(&ast, query_path.clone())?; + + generate_module_token_stream(query_path, &schema_path, options) + .map(Into::into) + .map_err(|err| { + syn::Error::new_spanned( + ast, + format!("Failed to generate GraphQLQuery impl: {}", err), + ) + }) } -fn build_query_and_schema_path( - input: &syn::DeriveInput, -) -> Result<(PathBuf, PathBuf), failure::Error> { - let cargo_manifest_dir = ::std::env::var("CARGO_MANIFEST_DIR") - .context("Checking that the CARGO_MANIFEST_DIR env variable is defined.")?; +fn build_query_and_schema_path(input: &syn::DeriveInput) -> Result<(PathBuf, PathBuf), syn::Error> { + let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_err| { + syn::Error::new_spanned( + input, + "Error checking that the CARGO_MANIFEST_DIR env variable is defined.", + ) + })?; - let query_path = - attributes::extract_attr(input, "query_path").context("Extracting query path.")?; + let query_path = attributes::extract_attr(input, "query_path")?; let query_path = format!("{}/{}", cargo_manifest_dir, query_path); let query_path = Path::new(&query_path).to_path_buf(); - let schema_path = - attributes::extract_attr(input, "schema_path").context("Extracting schema path.")?; + let schema_path = attributes::extract_attr(input, "schema_path")?; let schema_path = Path::new(&cargo_manifest_dir).join(schema_path); Ok((query_path, schema_path)) } @@ -60,14 +58,27 @@ fn build_query_and_schema_path( fn build_graphql_client_derive_options( input: &syn::DeriveInput, query_path: PathBuf, -) -> Result { +) -> Result { + let variables_derives = attributes::extract_attr(input, "variables_derives").ok(); let response_derives = attributes::extract_attr(input, "response_derives").ok(); + let custom_scalars_module = attributes::extract_attr(input, "custom_scalars_module").ok(); + let extern_enums = attributes::extract_attr_list(input, "extern_enums").ok(); + let fragments_other_variant: bool = attributes::extract_fragments_other_variant(input); + let skip_serializing_none: bool = attributes::extract_skip_serializing_none(input); + let custom_variable_types = attributes::extract_attr_list(input, "variable_types").ok(); + let custom_response_type = attributes::extract_attr(input, "response_type").ok(); let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Derive); options.set_query_file(query_path); + options.set_fragments_other_variant(fragments_other_variant); + options.set_skip_serializing_none(skip_serializing_none); + + if let Some(variables_derives) = variables_derives { + options.set_variables_derives(variables_derives); + }; if let Some(response_derives) = response_derives { - options.set_additional_derives(response_derives); + options.set_response_derives(response_derives); }; // The user can determine what to do about deprecations. @@ -75,9 +86,35 @@ fn build_graphql_client_derive_options( options.set_deprecation_strategy(deprecation_strategy); }; + // The user can specify the normalization strategy. + if let Ok(normalization) = attributes::extract_normalization(input) { + options.set_normalization(normalization); + }; + + // The user can give a path to a module that provides definitions for the custom scalars. + if let Some(custom_scalars_module) = custom_scalars_module { + let custom_scalars_module = syn::parse_str(&custom_scalars_module)?; + + options.set_custom_scalars_module(custom_scalars_module); + } + + // The user can specify a list of enums types that are defined externally, rather than generated by this library + if let Some(extern_enums) = extern_enums { + options.set_extern_enums(extern_enums); + } + + if let Some(custom_variable_types) = custom_variable_types { + options.set_custom_variable_types(custom_variable_types); + } + + if let Some(custom_response_type) = custom_response_type { + options.set_custom_response_type(custom_response_type); + } + options.set_struct_ident(input.ident.clone()); options.set_module_visibility(input.vis.clone()); options.set_operation_name(input.ident.to_string()); + options.set_serde_path(syn::parse_quote!(graphql_client::_private::serde)); Ok(options) }