├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── github │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ └── examples │ │ ├── github.rs │ │ ├── query_1.graphql │ │ └── schema.graphql ├── hasura │ ├── Cargo.toml │ ├── README.md │ └── examples │ │ ├── hasura.rs │ │ ├── query_1.graphql │ │ └── schema.graphql └── web │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ ├── schema.json │ └── src │ ├── lib.rs │ └── puppy_smiles.graphql ├── graphql-introspection-query ├── Cargo.toml ├── README.md └── src │ ├── introspection_response.rs │ └── lib.rs ├── graphql_client ├── Cargo.toml ├── src │ ├── lib.rs │ ├── reqwest.rs │ └── serde_with.rs └── tests │ ├── Germany.graphql │ ├── alias.rs │ ├── alias │ ├── query.graphql │ └── schema.graphql │ ├── countries_schema.json │ ├── custom_scalars.rs │ ├── custom_scalars │ ├── query.graphql │ └── schema.graphql │ ├── default.rs │ ├── default │ ├── query.graphql │ └── schema.graphql │ ├── deprecation.rs │ ├── deprecation │ ├── query.graphql │ └── schema.graphql │ ├── extern_enums.rs │ ├── extern_enums │ ├── multiple_extern_enums_query.graphql │ ├── multiple_extern_enums_response.json │ ├── schema.graphql │ ├── single_extern_enum_query.graphql │ └── single_extern_enum_response.json │ ├── fragment_chain.rs │ ├── fragment_chain │ ├── query.graphql │ └── schema.graphql │ ├── fragments.rs │ ├── fragments │ ├── query.graphql │ └── schema.graphql │ ├── input_object_variables.rs │ ├── input_object_variables │ ├── input_object_variables_query.graphql │ ├── input_object_variables_query_defaults.graphql │ └── input_object_variables_schema.graphql │ ├── int_id.rs │ ├── int_id │ ├── query.graphql │ └── schema.graphql │ ├── interfaces.rs │ ├── interfaces │ ├── interface_not_on_everything_query.graphql │ ├── interface_query.graphql │ ├── interface_response.json │ ├── interface_response_not_on_everything.json │ ├── interface_schema.graphql │ ├── interface_with_fragment_query.graphql │ ├── interface_with_fragment_response.json │ └── interface_with_type_refining_fragment_query.graphql │ ├── introspection.rs │ ├── introspection │ ├── introspection_query.graphql │ ├── introspection_response.json │ └── introspection_schema.graphql │ ├── json_schema.rs │ ├── json_schema │ ├── query.graphql │ ├── query_2.graphql │ ├── schema_1.json │ └── schema_2.json │ ├── more_derives.rs │ ├── more_derives │ ├── query.graphql │ └── schema.graphql │ ├── one_of_input.rs │ ├── one_of_input │ ├── query.graphql │ └── schema.graphql │ ├── operation_selection.rs │ ├── operation_selection │ ├── queries.graphql │ └── schema.graphql │ ├── scalar_variables.rs │ ├── scalar_variables │ ├── scalar_variables_query.graphql │ ├── scalar_variables_query_defaults.graphql │ └── scalar_variables_schema.graphql │ ├── skip_serializing_none.rs │ ├── skip_serializing_none │ ├── query.graphql │ └── schema.graphql │ ├── subscription │ ├── subscription_invalid_query.graphql │ ├── subscription_query.graphql │ ├── subscription_query_response.json │ └── subscription_schema.graphql │ ├── subscriptions.rs │ ├── type_refining_fragments.rs │ ├── union_query.rs │ └── unions │ ├── fragment_and_more_response.json │ ├── type_refining_fragment_on_union_query.graphql │ ├── union_query.graphql │ ├── union_query_response.json │ └── union_schema.graphql ├── graphql_client_cli ├── .gitignore ├── Cargo.toml ├── README.md └── src │ ├── error.rs │ ├── generate.rs │ ├── graphql │ ├── introspection_query.graphql │ ├── introspection_query_with_isOneOf_specifiedByUrl.graphql │ ├── introspection_query_with_is_one_of.graphql │ ├── introspection_query_with_specified_by.graphql │ └── introspection_schema.graphql │ ├── introspection_queries.rs │ ├── introspection_schema.rs │ └── main.rs ├── graphql_client_codegen ├── Cargo.toml └── src │ ├── codegen.rs │ ├── codegen │ ├── enums.rs │ ├── inputs.rs │ ├── selection.rs │ └── shared.rs │ ├── codegen_options.rs │ ├── constants.rs │ ├── deprecation.rs │ ├── generated_module.rs │ ├── lib.rs │ ├── normalization.rs │ ├── query.rs │ ├── query │ ├── fragments.rs │ ├── operations.rs │ ├── selection.rs │ └── validation.rs │ ├── schema.rs │ ├── schema │ ├── graphql_parser_conversion.rs │ ├── json_conversion.rs │ ├── tests.rs │ └── tests │ │ ├── extend_object.rs │ │ ├── extend_object_schema.graphql │ │ ├── extend_object_schema.json │ │ ├── github.rs │ │ ├── github_schema.graphql │ │ └── github_schema.json │ ├── tests │ ├── foobars_query.graphql │ ├── foobars_schema.graphql │ ├── keywords_query.graphql │ ├── keywords_schema.graphql │ ├── mod.rs │ ├── star_wars_query.graphql │ └── star_wars_schema.graphql │ └── type_qualifiers.rs └── graphql_query_derive ├── Cargo.toml └── src ├── attributes.rs └── lib.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.rs] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 4 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | Cargo.lock binary 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # bump major and minor updates as soon as available 4 | - package-ecosystem: cargo 5 | target-branch: main # see https://github.com/dependabot/dependabot-core/issues/1778#issuecomment-1988140219 6 | directory: / 7 | schedule: 8 | interval: daily 9 | commit-message: 10 | prefix: chore 11 | include: scope 12 | ignore: 13 | - dependency-name: "*" 14 | update-types: 15 | - "version-update:semver-patch" 16 | 17 | # bundle patch updates together on a monthly basis 18 | - package-ecosystem: cargo 19 | directory: / 20 | schedule: 21 | interval: monthly 22 | commit-message: 23 | prefix: chore 24 | include: scope 25 | groups: 26 | patch-updates: 27 | update-types: 28 | - patch 29 | ignore: 30 | - dependency-name: "*" 31 | update-types: 32 | - "version-update:semver-minor" 33 | - "version-update:semver-major" 34 | 35 | # bump actions as soon as available 36 | - package-ecosystem: github-actions 37 | directory: / 38 | schedule: 39 | interval: daily 40 | commit-message: 41 | prefix: chore 42 | include: scope 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | schedule: 11 | - cron: '0 2 * * *' 12 | 13 | env: 14 | clippy_rust_version: '1.82' 15 | 16 | jobs: 17 | test: 18 | strategy: 19 | matrix: 20 | rust: ["stable", "beta", "nightly"] 21 | os: [ubuntu-latest, macos-latest] 22 | name: Cargo test 23 | runs-on: ${{ matrix.os }} 24 | if: github.repository == 'graphql-rust/graphql-client' 25 | steps: 26 | - name: Checkout sources 27 | uses: actions/checkout@v4 28 | - name: Install toolchain 29 | uses: dtolnay/rust-toolchain@master 30 | with: 31 | toolchain: ${{ matrix.rust }} 32 | - name: Execute cargo test 33 | run: cargo test --all --tests --examples 34 | wasm_build: 35 | name: Cargo build for wasm 36 | runs-on: ubuntu-latest 37 | if: github.repository == 'graphql-rust/graphql-client' 38 | steps: 39 | - name: Checkout sources 40 | uses: actions/checkout@v4 41 | - name: Install toolchain 42 | uses: dtolnay/rust-toolchain@stable 43 | with: 44 | target: wasm32-unknown-unknown 45 | - name: Execute cargo build 46 | run: | 47 | cargo build --manifest-path=./graphql_client/Cargo.toml --features="reqwest" --target wasm32-unknown-unknown 48 | 49 | rustfmt: 50 | name: Rustfmt 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v4 54 | - uses: dtolnay/rust-toolchain@stable 55 | - run: cargo fmt --all -- --check 56 | 57 | lint: 58 | name: Clippy 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v4 62 | - uses: dtolnay/rust-toolchain@master 63 | with: 64 | toolchain: ${{ env.clippy_rust_version }} 65 | components: clippy 66 | - run: cargo clippy --all --all-targets --all-features -- -D warnings 67 | 68 | msrv: 69 | runs-on: ubuntu-latest 70 | steps: 71 | - uses: actions/checkout@master 72 | - name: Get MSRV from Cargo.toml 73 | run: | 74 | MSRV=$(grep 'rust-version' Cargo.toml | sed 's/.*= *"\(.*\)".*/\1/') 75 | echo "MSRV=$MSRV" >> $GITHUB_ENV 76 | - uses: dtolnay/rust-toolchain@master 77 | with: 78 | toolchain: ${{ env.MSRV }} 79 | - uses: taiki-e/install-action@cargo-no-dev-deps 80 | - run: cargo no-dev-deps check -p graphql_client 81 | 82 | # Automatically merge if it's a Dependabot PR that passes the build 83 | dependabot: 84 | needs: [test, wasm_build, lint, msrv] 85 | permissions: 86 | contents: write 87 | pull-requests: write 88 | runs-on: ubuntu-latest 89 | if: github.actor == 'dependabot[bot]' 90 | steps: 91 | - name: Enable auto-merge for Dependabot PRs 92 | run: gh pr merge --auto --merge "$PR_URL" 93 | env: 94 | PR_URL: ${{github.event.pull_request.html_url}} 95 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | node_modules/ 3 | **/*.rs.bk 4 | .idea 5 | scripts/* 6 | !scripts/*.sh 7 | /.vscode 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | target/ 2 | node_modules/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | .idea -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at tom@tomhoule.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing 3 | 4 | All contributors are expected to follow our [Code of Conduct](CODE_OF_CONDUCT.md). 5 | 6 | ## Pull requests 7 | 8 | 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. 9 | 10 | ## Testing 11 | 12 | The CI system conducts a few different tests for various releases of rust. In addition to the normal cargo tests, code formatting is checked with [fmt](https://github.com/rust-lang-nursery/rustfmt), and linting is checked with [clippy](https://github.com/rust-lang-nursery/rust-clippy). Whereas cargo tests are run for all rust release channels, `fmt` and `clippy` are only run on the stable channel. 13 | 14 | | Channel | fmt | clippy | test | 15 | |---------|-----|--------|------| 16 | | stable | x | x | x | 17 | | beta | | | x | 18 | | nightly | | | x | 19 | 20 | To avoid any surprises by CI while merging, it's recommended you run these locally after making changes. Setup and testing only takes a couple minutes at most. 21 | 22 | ### Setup 23 | 24 | Rust does not have `fmt` or `clippy` installed by default, so you will have to add them manually. The installation process is unlikely to change, but if it does, specific installation instructions can be found on the READMEs for [fmt](https://github.com/rust-lang-nursery/rustfmt#quick-start) and [clippy](https://github.com/rust-lang-nursery/rust-clippy#step-2-install-clippy). 25 | 26 | ``` 27 | rustup component add rustfmt-preview clippy-preview 28 | ``` 29 | 30 | If you want install to a different toolchain (if for instance your default is set to nightly, but you need to test stable), you can provide the 'toolchain' argument: 31 | 32 | ``` 33 | rustup component add rustfmt-preview clippy-preview --toolchain stable 34 | ``` 35 | 36 | We are using [Prettier](https://prettier.io) to check `.json|.graphql` files. To have it on your local machine you need to install [Node.js](https://nodejs.org) first. 37 | Our build is now using latest LTS version of Node.js. We're using `npm` and global install here: 38 | 39 | ```bash 40 | npm install --global prettier 41 | ``` 42 | 43 | ### Running 44 | 45 | If you are on the stable channel, then you can run fmt, clippy, and tests. 46 | 47 | ``` 48 | cargo fmt --all -- --check 49 | cargo clippy 50 | cargo test --all 51 | ``` 52 | 53 | If your default channel is something other than stable, you can force the use of stable by providing the channel option: 54 | 55 | ``` 56 | cargo +stable fmt --all -- --check 57 | cargo +stable clippy 58 | cargo +stable test --all 59 | ``` 60 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "graphql_client", 5 | "graphql_client_cli", 6 | "graphql_client_codegen", 7 | "graphql-introspection-query", 8 | "graphql_query_derive", 9 | 10 | # Example crates. 11 | "examples/*", 12 | ] 13 | 14 | [workspace.package] 15 | rust-version = "1.64.0" 16 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2018 Tom Houle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/github/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.env 3 | -------------------------------------------------------------------------------- /examples/github/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphql_query_github_example" 3 | version = "0.1.0" 4 | authors = ["Tom Houlé "] 5 | edition = "2018" 6 | 7 | [dev-dependencies] 8 | anyhow = "1.0" 9 | graphql_client = { path = "../../graphql_client", features = ["reqwest-blocking"] } 10 | reqwest = { version = "0.12", features = ["json", "blocking"] } 11 | prettytable-rs = "^0.10.0" 12 | clap = { version = "^4.0", features = ["derive"] } 13 | log = "^0.4" 14 | env_logger = "0.10.2" 15 | -------------------------------------------------------------------------------- /examples/github/README.md: -------------------------------------------------------------------------------- 1 | # graphql-client GitHub API examples 2 | 3 | The schema is taken from [this repo](https://raw.githubusercontent.com/octokit/graphql-schema/master/schema.graphql). 4 | 5 | ## How to run it 6 | 7 | The example expects to find a valid GitHub API Token in the environment (`GITHUB_API_TOKEN`). See the [GitHub documentation](https://developer.github.com/v4/guides/forming-calls/#authenticating-with-graphql) on how to generate one. 8 | 9 | Then just run the example with a repository name as argument. For example: 10 | 11 | ```bash 12 | cargo run --example github graphql-rust/graphql-client 13 | ``` 14 | -------------------------------------------------------------------------------- /examples/github/examples/github.rs: -------------------------------------------------------------------------------- 1 | use ::reqwest::blocking::Client; 2 | use anyhow::*; 3 | use clap::Parser; 4 | use graphql_client::{reqwest::post_graphql_blocking as post_graphql, GraphQLQuery}; 5 | use log::*; 6 | use prettytable::*; 7 | 8 | #[allow(clippy::upper_case_acronyms)] 9 | type URI = String; 10 | 11 | #[derive(GraphQLQuery)] 12 | #[graphql( 13 | schema_path = "examples/schema.graphql", 14 | query_path = "examples/query_1.graphql", 15 | response_derives = "Debug" 16 | )] 17 | struct RepoView; 18 | 19 | #[derive(Parser)] 20 | #[clap(author, about, version)] 21 | struct Command { 22 | #[clap(name = "repository")] 23 | repo: String, 24 | } 25 | 26 | fn parse_repo_name(repo_name: &str) -> Result<(&str, &str), anyhow::Error> { 27 | let mut parts = repo_name.split('/'); 28 | match (parts.next(), parts.next()) { 29 | (Some(owner), Some(name)) => Ok((owner, name)), 30 | _ => Err(format_err!("wrong format for the repository name param (we expect something like facebook/graphql)")) 31 | } 32 | } 33 | 34 | fn main() -> Result<(), anyhow::Error> { 35 | env_logger::init(); 36 | 37 | let github_api_token = 38 | std::env::var("GITHUB_API_TOKEN").expect("Missing GITHUB_API_TOKEN env var"); 39 | 40 | let args = Command::parse(); 41 | 42 | let repo = args.repo; 43 | let (owner, name) = parse_repo_name(&repo).unwrap_or(("tomhoule", "graphql-client")); 44 | 45 | let variables = repo_view::Variables { 46 | owner: owner.to_string(), 47 | name: name.to_string(), 48 | }; 49 | 50 | let client = Client::builder() 51 | .user_agent("graphql-rust/0.10.0") 52 | .default_headers( 53 | std::iter::once(( 54 | reqwest::header::AUTHORIZATION, 55 | reqwest::header::HeaderValue::from_str(&format!("Bearer {}", github_api_token)) 56 | .unwrap(), 57 | )) 58 | .collect(), 59 | ) 60 | .build()?; 61 | 62 | let response_body = 63 | post_graphql::(&client, "https://api.github.com/graphql", variables).unwrap(); 64 | 65 | info!("{:?}", response_body); 66 | 67 | let response_data: repo_view::ResponseData = response_body.data.expect("missing response data"); 68 | 69 | let stars: Option = response_data 70 | .repository 71 | .as_ref() 72 | .map(|repo| repo.stargazers.total_count); 73 | 74 | println!("{}/{} - 🌟 {}", owner, name, stars.unwrap_or(0),); 75 | 76 | let mut table = prettytable::Table::new(); 77 | table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE); 78 | table.set_titles(row!(b => "issue", "comments")); 79 | 80 | for issue in response_data 81 | .repository 82 | .expect("missing repository") 83 | .issues 84 | .nodes 85 | .expect("issue nodes is null") 86 | .iter() 87 | .flatten() 88 | { 89 | table.add_row(row!(issue.title, issue.comments.total_count)); 90 | } 91 | 92 | table.printstd(); 93 | Ok(()) 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use super::*; 99 | 100 | #[test] 101 | fn parse_repo_name_works() { 102 | assert_eq!( 103 | parse_repo_name("graphql-rust/graphql-client").unwrap(), 104 | ("graphql-rust", "graphql-client") 105 | ); 106 | assert!(parse_repo_name("abcd").is_err()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /examples/github/examples/query_1.graphql: -------------------------------------------------------------------------------- 1 | query RepoView($owner: String!, $name: String!) { 2 | repository(owner: $owner, name: $name) { 3 | homepageUrl 4 | stargazers { 5 | totalCount 6 | } 7 | issues(first: 20, states: OPEN) { 8 | nodes { 9 | title 10 | comments { 11 | totalCount 12 | } 13 | } 14 | } 15 | pullRequests(first: 20, states: OPEN) { 16 | nodes { 17 | title 18 | commits { 19 | totalCount 20 | } 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/hasura/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphql_query_hasura_example" 3 | version = "0.1.0" 4 | authors = ["Mark Catley "] 5 | edition = "2018" 6 | 7 | [dev-dependencies] 8 | anyhow = "1.0" 9 | graphql_client = { path = "../../graphql_client", features = ["reqwest-blocking"] } 10 | serde_json = "1.0" 11 | reqwest = { version = "0.12", features = ["json", "blocking"] } 12 | prettytable-rs = "0.10.0" 13 | log = "0.4.3" 14 | env_logger = "0.10.2" 15 | -------------------------------------------------------------------------------- /examples/hasura/README.md: -------------------------------------------------------------------------------- 1 | # graphql-client Hasura examples 2 | 3 | The schema was generated using [Hasura](https://hasura.io/). It is here to 4 | demonstrate the `normalization` attribute and would require some work to 5 | create a Hasura instance that matches the schema. It is primarily present to 6 | ensure the attribute behaves correctly. 7 | -------------------------------------------------------------------------------- /examples/hasura/examples/hasura.rs: -------------------------------------------------------------------------------- 1 | use ::reqwest::blocking::Client; 2 | use graphql_client::{reqwest::post_graphql_blocking as post_graphql, GraphQLQuery}; 3 | use log::*; 4 | use prettytable::*; 5 | 6 | type Bpchar = String; 7 | type Timestamptz = String; 8 | 9 | #[derive(GraphQLQuery)] 10 | #[graphql( 11 | schema_path = "examples/schema.graphql", 12 | query_path = "examples/query_1.graphql", 13 | response_derives = "Debug", 14 | normalization = "rust" 15 | )] 16 | struct UpsertIssue; 17 | 18 | fn main() -> Result<(), anyhow::Error> { 19 | use upsert_issue::{IssuesUpdateColumn::*, *}; 20 | env_logger::init(); 21 | 22 | let v = Variables { 23 | issues: vec![IssuesInsertInput { 24 | id: Some("001000000000000".to_string()), 25 | name: Some("Name".to_string()), 26 | status: Some("Draft".to_string()), 27 | salesforce_updated_at: Some("2019-06-11T08:14:28Z".to_string()), 28 | }], 29 | update_columns: vec![Name, Status, SalesforceUpdatedAt], 30 | }; 31 | 32 | let client = Client::new(); 33 | 34 | let response_body = 35 | post_graphql::(&client, "https://localhost:8080/v1/graphql", v)?; 36 | info!("{:?}", response_body); 37 | 38 | if let Some(errors) = response_body.errors { 39 | error!("there are errors:"); 40 | 41 | for error in &errors { 42 | error!("{:?}", error); 43 | } 44 | } 45 | 46 | let response_data = response_body.data.expect("missing response data"); 47 | 48 | let mut table = prettytable::Table::new(); 49 | 50 | table.add_row(row!(b => "id", "name")); 51 | 52 | for issue in &response_data 53 | .insert_issues 54 | .expect("Inserted Issues") 55 | .returning 56 | { 57 | table.add_row(row!(issue.id, issue.name)); 58 | } 59 | 60 | table.printstd(); 61 | Ok(()) 62 | } 63 | -------------------------------------------------------------------------------- /examples/hasura/examples/query_1.graphql: -------------------------------------------------------------------------------- 1 | mutation upsert_issue( 2 | $issues: [issues_insert_input!]! 3 | $update_columns: [issues_update_column!]! 4 | ) { 5 | insert_issues( 6 | objects: $issues 7 | on_conflict: { constraint: issues_pkey, update_columns: $update_columns } 8 | ) { 9 | returning { 10 | id 11 | name 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/hasura/examples/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: query_root 3 | mutation: mutation_root 4 | subscription: subscription_root 5 | } 6 | 7 | scalar bpchar 8 | 9 | # expression to compare columns of type bpchar. All fields are combined with logical 'AND'. 10 | input bpchar_comparison_exp { 11 | _eq: bpchar 12 | _gt: bpchar 13 | _gte: bpchar 14 | _in: [bpchar!] 15 | _is_null: Boolean 16 | _lt: bpchar 17 | _lte: bpchar 18 | _neq: bpchar 19 | _nin: [bpchar!] 20 | } 21 | 22 | # conflict action 23 | enum conflict_action { 24 | # ignore the insert on this row 25 | ignore 26 | 27 | # update the row with the given values 28 | update 29 | } 30 | 31 | # columns and relationships of "issues" 32 | type issues { 33 | id: bpchar! 34 | name: String! 35 | salesforce_updated_at: timestamptz! 36 | status: String! 37 | } 38 | 39 | # aggregated selection of "issues" 40 | type issues_aggregate { 41 | aggregate: issues_aggregate_fields 42 | nodes: [issues!]! 43 | } 44 | 45 | # aggregate fields of "issues" 46 | type issues_aggregate_fields { 47 | count(columns: [issues_select_column!], distinct: Boolean): Int 48 | max: issues_max_fields 49 | min: issues_min_fields 50 | } 51 | 52 | # order by aggregate values of table "issues" 53 | input issues_aggregate_order_by { 54 | count: order_by 55 | max: issues_max_order_by 56 | min: issues_min_order_by 57 | } 58 | 59 | # input type for inserting array relation for remote table "issues" 60 | input issues_arr_rel_insert_input { 61 | data: [issues_insert_input!]! 62 | on_conflict: issues_on_conflict 63 | } 64 | 65 | # Boolean expression to filter rows from the table "issues". All fields are combined with a logical 'AND'. 66 | input issues_bool_exp { 67 | _and: [issues_bool_exp] 68 | _not: issues_bool_exp 69 | _or: [issues_bool_exp] 70 | id: bpchar_comparison_exp 71 | name: text_comparison_exp 72 | salesforce_updated_at: timestamptz_comparison_exp 73 | status: text_comparison_exp 74 | } 75 | 76 | # unique or primary key constraints on table "issues" 77 | enum issues_constraint { 78 | # unique or primary key constraint 79 | issues_pkey 80 | } 81 | 82 | # input type for inserting data into table "issues" 83 | input issues_insert_input { 84 | id: bpchar 85 | name: String 86 | salesforce_updated_at: timestamptz 87 | status: String 88 | } 89 | 90 | # aggregate max on columns 91 | type issues_max_fields { 92 | name: String 93 | salesforce_updated_at: timestamptz 94 | status: String 95 | } 96 | 97 | # order by max() on columns of table "issues" 98 | input issues_max_order_by { 99 | name: order_by 100 | salesforce_updated_at: order_by 101 | status: order_by 102 | } 103 | 104 | # aggregate min on columns 105 | type issues_min_fields { 106 | name: String 107 | salesforce_updated_at: timestamptz 108 | status: String 109 | } 110 | 111 | # order by min() on columns of table "issues" 112 | input issues_min_order_by { 113 | name: order_by 114 | salesforce_updated_at: order_by 115 | status: order_by 116 | } 117 | 118 | # response of any mutation on the table "issues" 119 | type issues_mutation_response { 120 | # number of affected rows by the mutation 121 | affected_rows: Int! 122 | 123 | # data of the affected rows by the mutation 124 | returning: [issues!]! 125 | } 126 | 127 | # input type for inserting object relation for remote table "issues" 128 | input issues_obj_rel_insert_input { 129 | data: issues_insert_input! 130 | on_conflict: issues_on_conflict 131 | } 132 | 133 | # on conflict condition type for table "issues" 134 | input issues_on_conflict { 135 | constraint: issues_constraint! 136 | update_columns: [issues_update_column!]! 137 | } 138 | 139 | # ordering options when selecting data from "issues" 140 | input issues_order_by { 141 | id: order_by 142 | name: order_by 143 | salesforce_updated_at: order_by 144 | status: order_by 145 | } 146 | 147 | # select columns of table "issues" 148 | enum issues_select_column { 149 | # column name 150 | id 151 | 152 | # column name 153 | name 154 | 155 | # column name 156 | salesforce_updated_at 157 | 158 | # column name 159 | status 160 | } 161 | 162 | # input type for updating data in table "issues" 163 | input issues_set_input { 164 | id: bpchar 165 | name: String 166 | salesforce_updated_at: timestamptz 167 | status: String 168 | } 169 | 170 | # update columns of table "issues" 171 | enum issues_update_column { 172 | # column name 173 | id 174 | 175 | # column name 176 | name 177 | 178 | # column name 179 | salesforce_updated_at 180 | 181 | # column name 182 | status 183 | } 184 | 185 | # mutation root 186 | type mutation_root { 187 | # delete data from the table: "issues" 188 | delete_issues( 189 | # filter the rows which have to be deleted 190 | where: issues_bool_exp! 191 | ): issues_mutation_response 192 | 193 | # insert data into the table: "issues" 194 | insert_issues( 195 | # the rows to be inserted 196 | objects: [issues_insert_input!]! 197 | 198 | # on conflict condition 199 | on_conflict: issues_on_conflict 200 | ): issues_mutation_response 201 | 202 | # update data of the table: "issues" 203 | update_issues( 204 | # sets the columns of the filtered rows to the given values 205 | _set: issues_set_input 206 | 207 | # filter the rows which have to be updated 208 | where: issues_bool_exp! 209 | ): issues_mutation_response 210 | } 211 | 212 | # column ordering options 213 | enum order_by { 214 | # in the ascending order, nulls last 215 | asc 216 | 217 | # in the ascending order, nulls first 218 | asc_nulls_first 219 | 220 | # in the ascending order, nulls last 221 | asc_nulls_last 222 | 223 | # in the descending order, nulls first 224 | desc 225 | 226 | # in the descending order, nulls first 227 | desc_nulls_first 228 | 229 | # in the descending order, nulls last 230 | desc_nulls_last 231 | } 232 | 233 | # query root 234 | type query_root { 235 | # fetch data from the table: "issues" 236 | issues( 237 | # distinct select on columns 238 | distinct_on: [issues_select_column!] 239 | 240 | # limit the nuber of rows returned 241 | limit: Int 242 | 243 | # skip the first n rows. Use only with order_by 244 | offset: Int 245 | 246 | # sort the rows by one or more columns 247 | order_by: [issues_order_by!] 248 | 249 | # filter the rows returned 250 | where: issues_bool_exp 251 | ): [issues!]! 252 | 253 | # fetch aggregated fields from the table: "issues" 254 | issues_aggregate( 255 | # distinct select on columns 256 | distinct_on: [issues_select_column!] 257 | 258 | # limit the nuber of rows returned 259 | limit: Int 260 | 261 | # skip the first n rows. Use only with order_by 262 | offset: Int 263 | 264 | # sort the rows by one or more columns 265 | order_by: [issues_order_by!] 266 | 267 | # filter the rows returned 268 | where: issues_bool_exp 269 | ): issues_aggregate! 270 | 271 | # fetch data from the table: "issues" using primary key columns 272 | issues_by_pk(id: bpchar!): issues 273 | } 274 | 275 | # subscription root 276 | type subscription_root { 277 | # fetch data from the table: "issues" 278 | issues( 279 | # distinct select on columns 280 | distinct_on: [issues_select_column!] 281 | 282 | # limit the nuber of rows returned 283 | limit: Int 284 | 285 | # skip the first n rows. Use only with order_by 286 | offset: Int 287 | 288 | # sort the rows by one or more columns 289 | order_by: [issues_order_by!] 290 | 291 | # filter the rows returned 292 | where: issues_bool_exp 293 | ): [issues!]! 294 | 295 | # fetch aggregated fields from the table: "issues" 296 | issues_aggregate( 297 | # distinct select on columns 298 | distinct_on: [issues_select_column!] 299 | 300 | # limit the nuber of rows returned 301 | limit: Int 302 | 303 | # skip the first n rows. Use only with order_by 304 | offset: Int 305 | 306 | # sort the rows by one or more columns 307 | order_by: [issues_order_by!] 308 | 309 | # filter the rows returned 310 | where: issues_bool_exp 311 | ): issues_aggregate! 312 | 313 | # fetch data from the table: "issues" using primary key columns 314 | issues_by_pk(id: bpchar!): issues 315 | } 316 | 317 | # expression to compare columns of type text. All fields are combined with logical 'AND'. 318 | input text_comparison_exp { 319 | _eq: String 320 | _gt: String 321 | _gte: String 322 | _ilike: String 323 | _in: [String!] 324 | _is_null: Boolean 325 | _like: String 326 | _lt: String 327 | _lte: String 328 | _neq: String 329 | _nilike: String 330 | _nin: [String!] 331 | _nlike: String 332 | _nsimilar: String 333 | _similar: String 334 | } 335 | 336 | scalar timestamptz 337 | 338 | # expression to compare columns of type timestamptz. All fields are combined with logical 'AND'. 339 | input timestamptz_comparison_exp { 340 | _eq: timestamptz 341 | _gt: timestamptz 342 | _gte: timestamptz 343 | _in: [timestamptz!] 344 | _is_null: Boolean 345 | _lt: timestamptz 346 | _lte: timestamptz 347 | _neq: timestamptz 348 | _nin: [timestamptz!] 349 | } 350 | -------------------------------------------------------------------------------- /examples/web/.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | web_bg.d.ts 3 | web_bg.wasm 4 | web.d.ts 5 | web.d.ts 6 | web.js 7 | -------------------------------------------------------------------------------- /examples/web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "web" 3 | version = "0.1.0" 4 | authors = ["Tom Houlé "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | graphql_client = { path = "../../graphql_client", features = ["reqwest"] } 12 | wasm-bindgen = "^0.2" 13 | lazy_static = "1.0.1" 14 | js-sys = "0.3.6" 15 | wasm-bindgen-futures = "0.4.18" 16 | reqwest = "0.12" 17 | 18 | [dependencies.web-sys] 19 | version = "0.3.6" 20 | features = [ 21 | "console", 22 | "Document", 23 | "Element", 24 | "EventTarget", 25 | "Node", 26 | "HtmlBodyElement", 27 | "HtmlDocument", 28 | "HtmlElement", 29 | "Window", 30 | ] 31 | -------------------------------------------------------------------------------- /examples/web/README.md: -------------------------------------------------------------------------------- 1 | # call from JS example 2 | 3 | This is a demo of the library compiled to WebAssembly for use in a browser. 4 | 5 | ## Build 6 | 7 | You will need the Rust toolchain and the 8 | [`wasm-pack`](https://rustwasm.github.io/wasm-pack/) CLI 9 | (`cargo install --force wasm-pack`) for this to work. To build 10 | the project, run the following command in this directory: 11 | 12 | ```bash 13 | wasm-pack build --target=web 14 | ``` 15 | 16 | The compiled WebAssembly program and the glue JS code will be 17 | located in the `./pkg` directory. To run the app, start an 18 | HTTP server in this directory - it contains an `index.html` 19 | file. For example, if you have Python 3: 20 | 21 | ```bash 22 | python3 -m http.server 8000 23 | ``` 24 | -------------------------------------------------------------------------------- /examples/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/web/src/lib.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::{reqwest::post_graphql, GraphQLQuery}; 2 | use lazy_static::*; 3 | use std::cell::RefCell; 4 | use std::sync::Mutex; 5 | use wasm_bindgen::prelude::*; 6 | use wasm_bindgen::JsCast; 7 | use wasm_bindgen_futures::future_to_promise; 8 | 9 | #[derive(GraphQLQuery)] 10 | #[graphql( 11 | schema_path = "schema.json", 12 | query_path = "src/puppy_smiles.graphql", 13 | response_derives = "Debug" 14 | )] 15 | struct PuppySmiles; 16 | 17 | fn log(s: &str) { 18 | web_sys::console::log_1(&JsValue::from_str(s)) 19 | } 20 | 21 | lazy_static! { 22 | static ref LAST_ENTRY: Mutex>> = Mutex::new(RefCell::new(None)); 23 | } 24 | 25 | async fn load_more() -> Result { 26 | let url = "https://www.graphqlhub.com/graphql"; 27 | let variables = puppy_smiles::Variables { 28 | after: LAST_ENTRY 29 | .lock() 30 | .ok() 31 | .and_then(|opt| opt.borrow().to_owned()), 32 | }; 33 | 34 | let client = reqwest::Client::new(); 35 | 36 | let response = post_graphql::(&client, url, variables) 37 | .await 38 | .map_err(|err| { 39 | log(&format!("Could not fetch puppies. error: {:?}", err)); 40 | JsValue::NULL 41 | })?; 42 | render_response(response); 43 | Ok(JsValue::NULL) 44 | } 45 | 46 | fn document() -> web_sys::Document { 47 | web_sys::window() 48 | .expect_throw("no window") 49 | .document() 50 | .expect_throw("no document") 51 | } 52 | 53 | fn add_load_more_button() { 54 | let btn = document() 55 | .create_element("button") 56 | .expect_throw("could not create button"); 57 | btn.set_inner_html("I WANT MORE PUPPIES"); 58 | let on_click = Closure::wrap( 59 | Box::new(move || future_to_promise(load_more())) as Box js_sys::Promise> 60 | ); 61 | btn.add_event_listener_with_callback( 62 | "click", 63 | on_click 64 | .as_ref() 65 | .dyn_ref() 66 | .expect_throw("on click is not a Function"), 67 | ) 68 | .expect_throw("could not add event listener to load more button"); 69 | 70 | let doc = document().body().expect_throw("no body"); 71 | doc.append_child(&btn) 72 | .expect_throw("could not append button"); 73 | 74 | on_click.forget(); 75 | } 76 | 77 | fn render_response(response: graphql_client::Response) { 78 | use std::fmt::Write; 79 | 80 | log(&format!("response body\n\n{:?}", response)); 81 | 82 | let parent = document().body().expect_throw("no body"); 83 | 84 | let json: graphql_client::Response = response; 85 | let response = document() 86 | .create_element("div") 87 | .expect_throw("could not create div"); 88 | let mut inner_html = String::new(); 89 | let listings = json 90 | .data 91 | .expect_throw("response data") 92 | .reddit 93 | .expect_throw("reddit") 94 | .subreddit 95 | .expect_throw("puppy smiles subreddit") 96 | .new_listings; 97 | 98 | let new_cursor: Option = listings[listings.len() - 1] 99 | .as_ref() 100 | .map(|puppy| puppy.fullname_id.clone()); 101 | LAST_ENTRY.lock().unwrap_throw().replace(new_cursor); 102 | 103 | for puppy in listings.iter().flatten() { 104 | write!( 105 | inner_html, 106 | r#" 107 |
108 | {} 109 |
110 |
{}
111 |
112 |
113 | "#, 114 | puppy.title, puppy.url, puppy.title 115 | ) 116 | .expect_throw("write to string"); 117 | } 118 | response.set_inner_html(&format!( 119 | "

response:

{}
", 120 | inner_html 121 | )); 122 | parent 123 | .append_child(&response) 124 | .expect_throw("could not append response"); 125 | } 126 | 127 | #[wasm_bindgen(start)] 128 | pub fn run() { 129 | log("Hello there"); 130 | let message_area = document() 131 | .create_element("div") 132 | .expect_throw("could not create div"); 133 | message_area.set_inner_html("

good morning

"); 134 | let parent = document().body().unwrap_throw(); 135 | parent 136 | .append_child(&message_area) 137 | .expect_throw("could not append message area"); 138 | 139 | add_load_more_button(); 140 | 141 | log("Bye"); 142 | } 143 | -------------------------------------------------------------------------------- /examples/web/src/puppy_smiles.graphql: -------------------------------------------------------------------------------- 1 | query PuppySmiles($after: String) { 2 | reddit { 3 | subreddit(name: "puppysmiles") { 4 | newListings(limit: 6, after: $after) { 5 | title 6 | fullnameId 7 | url 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /graphql-introspection-query/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphql-introspection-query" 3 | version = "0.2.0" 4 | authors = ["Tom Houlé "] 5 | edition = "2018" 6 | keywords = ["graphql", "api", "web"] 7 | categories = ["web-programming"] 8 | license = "Apache-2.0 OR MIT" 9 | repository = "https://github.com/graphql-rust/graphql-client" 10 | description = "GraphQL introspection query and response types." 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | serde = { version = "1.0", features = ["derive"] } 16 | -------------------------------------------------------------------------------- /graphql-introspection-query/README.md: -------------------------------------------------------------------------------- 1 | # graphql-introspection-query 2 | 3 | This crate defines structs implementing `serde::Deserialize` that match the shape returned by a spec-compliant GraphQL API presented with the introspection query. 4 | -------------------------------------------------------------------------------- /graphql-introspection-query/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod introspection_response; 2 | -------------------------------------------------------------------------------- /graphql_client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphql_client" 3 | version = "0.14.0" 4 | authors = ["Tom Houlé "] 5 | description = "Typed GraphQL requests and responses" 6 | repository = "https://github.com/graphql-rust/graphql-client" 7 | license = "Apache-2.0 OR MIT" 8 | keywords = ["graphql", "api", "web", "webassembly", "wasm"] 9 | categories = ["network-programming", "web-programming", "wasm"] 10 | edition = "2018" 11 | homepage = "https://github.com/graphql-rust/graphql-client" 12 | readme = "../README.md" 13 | rust-version.workspace = true 14 | 15 | [package.metadata.docs.rs] 16 | features = ["reqwest"] 17 | 18 | [dependencies] 19 | serde = { version = "1.0.78", features = ["derive"] } 20 | serde_json = "1.0.50" 21 | 22 | # Optional dependencies 23 | graphql_query_derive = { path = "../graphql_query_derive", version = "0.14.0", optional = true } 24 | reqwest-crate = { package = "reqwest", version = ">=0.11, <=0.12", features = ["json"], default-features = false, optional = true } 25 | 26 | [features] 27 | default = ["graphql_query_derive"] 28 | reqwest = ["reqwest-crate", "reqwest-crate/default-tls"] 29 | reqwest-rustls = ["reqwest-crate", "reqwest-crate/rustls-tls"] 30 | reqwest-blocking = ["reqwest-crate/blocking"] 31 | -------------------------------------------------------------------------------- /graphql_client/src/reqwest.rs: -------------------------------------------------------------------------------- 1 | //! A concrete client implementation over HTTP with reqwest. 2 | 3 | use crate::GraphQLQuery; 4 | use reqwest_crate as reqwest; 5 | 6 | /// Use the provided reqwest::Client to post a GraphQL request. 7 | #[cfg(any(feature = "reqwest", feature = "reqwest-rustls"))] 8 | pub async fn post_graphql( 9 | client: &reqwest::Client, 10 | url: U, 11 | variables: Q::Variables, 12 | ) -> Result, reqwest::Error> { 13 | let body = Q::build_query(variables); 14 | let reqwest_response = client.post(url).json(&body).send().await?; 15 | 16 | reqwest_response.json().await 17 | } 18 | 19 | /// Use the provided reqwest::Client to post a GraphQL request. 20 | #[cfg(feature = "reqwest-blocking")] 21 | pub fn post_graphql_blocking( 22 | client: &reqwest::blocking::Client, 23 | url: U, 24 | variables: Q::Variables, 25 | ) -> Result, reqwest::Error> { 26 | let body = Q::build_query(variables); 27 | let reqwest_response = client.post(url).json(&body).send()?; 28 | 29 | reqwest_response.json() 30 | } 31 | -------------------------------------------------------------------------------- /graphql_client/src/serde_with.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for overriding default serde implementations. 2 | 3 | use serde::{Deserialize, Deserializer}; 4 | 5 | #[derive(Deserialize)] 6 | #[serde(untagged)] 7 | enum IntOrString { 8 | Int(i64), 9 | Str(String), 10 | } 11 | 12 | impl From for String { 13 | fn from(value: IntOrString) -> Self { 14 | match value { 15 | IntOrString::Int(n) => n.to_string(), 16 | IntOrString::Str(s) => s, 17 | } 18 | } 19 | } 20 | 21 | /// Deserialize an optional ID type from either a String or an Integer representation. 22 | /// 23 | /// This is used by the codegen to enable String IDs to be deserialized from 24 | /// either Strings or Integers. 25 | pub fn deserialize_option_id<'de, D>(deserializer: D) -> Result, D::Error> 26 | where 27 | D: Deserializer<'de>, 28 | { 29 | Option::::deserialize(deserializer).map(|opt| opt.map(String::from)) 30 | } 31 | 32 | /// Deserialize an ID type from either a String or an Integer representation. 33 | /// 34 | /// This is used by the codegen to enable String IDs to be deserialized from 35 | /// either Strings or Integers. 36 | pub fn deserialize_id<'de, D>(deserializer: D) -> Result 37 | where 38 | D: Deserializer<'de>, 39 | { 40 | IntOrString::deserialize(deserializer).map(String::from) 41 | } 42 | -------------------------------------------------------------------------------- /graphql_client/tests/Germany.graphql: -------------------------------------------------------------------------------- 1 | query Germany { 2 | country(code: "DE") { 3 | name 4 | continent { 5 | name 6 | } 7 | } 8 | } 9 | 10 | query Country($countryCode: ID!) { 11 | country(code: $countryCode) { 12 | name 13 | continent { 14 | name 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /graphql_client/tests/alias.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | use serde_json::*; 3 | 4 | #[derive(GraphQLQuery)] 5 | #[graphql( 6 | query_path = "tests/alias/query.graphql", 7 | schema_path = "tests/alias/schema.graphql" 8 | )] 9 | pub struct AliasQuery; 10 | 11 | #[test] 12 | fn alias() { 13 | let valid_response = json!({ 14 | "alias": "127.0.1.2", 15 | "outer_alias": { 16 | "inner_alias": "inner value", 17 | }, 18 | }); 19 | 20 | let _type_name_test = alias_query::AliasQueryOuterAlias { inner_alias: None }; 21 | 22 | let valid_alias = serde_json::from_value::(valid_response).unwrap(); 23 | 24 | assert_eq!(valid_alias.alias.unwrap(), "127.0.1.2"); 25 | assert_eq!( 26 | valid_alias.outer_alias.unwrap().inner_alias.unwrap(), 27 | "inner value" 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /graphql_client/tests/alias/query.graphql: -------------------------------------------------------------------------------- 1 | query AliasQuery { 2 | alias: address 3 | outer_alias: nested { 4 | inner_alias: inner 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /graphql_client/tests/alias/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: QueryRoot 3 | } 4 | 5 | type QueryNest { 6 | inner: String 7 | } 8 | 9 | type QueryRoot { 10 | address: String 11 | nested: QueryNest 12 | } 13 | -------------------------------------------------------------------------------- /graphql_client/tests/custom_scalars.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | use serde_json::json; 3 | 4 | use std::net::Ipv4Addr; 5 | 6 | // Important! The NetworkAddress scalar should deserialize to an Ipv4Addr from the Rust std library. 7 | type NetworkAddress = Ipv4Addr; 8 | 9 | #[derive(GraphQLQuery)] 10 | #[graphql( 11 | query_path = "tests/custom_scalars/query.graphql", 12 | schema_path = "tests/custom_scalars/schema.graphql" 13 | )] 14 | pub struct CustomScalarQuery; 15 | 16 | #[test] 17 | fn custom_scalars() { 18 | let valid_response = json!({ 19 | "address": "127.0.1.2", 20 | }); 21 | 22 | let valid_addr = 23 | serde_json::from_value::(valid_response).unwrap(); 24 | 25 | assert_eq!( 26 | valid_addr.address.unwrap(), 27 | "127.0.1.2".parse::().unwrap() 28 | ); 29 | 30 | let invalid_response = json!({ 31 | "address": "localhost", 32 | }); 33 | 34 | assert!(serde_json::from_value::(invalid_response).is_err()); 35 | } 36 | -------------------------------------------------------------------------------- /graphql_client/tests/custom_scalars/query.graphql: -------------------------------------------------------------------------------- 1 | query CustomScalarQuery { 2 | address 3 | } 4 | -------------------------------------------------------------------------------- /graphql_client/tests/custom_scalars/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: QueryRoot 3 | } 4 | 5 | """ 6 | An IPv4 address 7 | """ 8 | scalar NetworkAddress 9 | 10 | type QueryRoot { 11 | address: NetworkAddress 12 | } 13 | -------------------------------------------------------------------------------- /graphql_client/tests/default.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | 3 | #[derive(GraphQLQuery)] 4 | #[graphql( 5 | query_path = "tests/default/query.graphql", 6 | schema_path = "tests/default/schema.graphql", 7 | variables_derives = "Default" 8 | )] 9 | struct OptQuery; 10 | 11 | #[test] 12 | fn variables_can_derive_default() { 13 | let _: ::Variables = Default::default(); 14 | } 15 | -------------------------------------------------------------------------------- /graphql_client/tests/default/query.graphql: -------------------------------------------------------------------------------- 1 | query OptQuery($param: Param) { 2 | optInput(query: $param) { 3 | name 4 | __typename 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /graphql_client/tests/default/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | } 4 | 5 | # The query type, represents all of the entry points into our object graph 6 | type Query { 7 | optInput(query: Param): Named 8 | } 9 | 10 | # What can be searched for. 11 | enum Param { 12 | AUTHOR 13 | } 14 | 15 | # A named entity 16 | type Named { 17 | # The ID of the entity 18 | id: ID! 19 | # The name of the entity 20 | name: String! 21 | } 22 | -------------------------------------------------------------------------------- /graphql_client/tests/deprecation.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | 3 | #[derive(GraphQLQuery)] 4 | #[graphql( 5 | schema_path = "tests/deprecation/schema.graphql", 6 | query_path = "tests/deprecation/query.graphql", 7 | deprecated = "allow" 8 | )] 9 | pub struct AllowDeprecation; 10 | 11 | #[derive(GraphQLQuery)] 12 | #[graphql( 13 | schema_path = "tests/deprecation/schema.graphql", 14 | query_path = "tests/deprecation/query.graphql", 15 | deprecated = "deny" 16 | )] 17 | pub struct DenyDeprecation; 18 | 19 | #[derive(GraphQLQuery)] 20 | #[graphql( 21 | schema_path = "tests/deprecation/schema.graphql", 22 | query_path = "tests/deprecation/query.graphql", 23 | deprecated = "warn" 24 | )] 25 | pub struct WarnDeprecation; 26 | 27 | #[test] 28 | fn deprecation_allow() { 29 | // Make any deprecations be a compile error. 30 | // Under `allow`, even deprecated fields aren't marked as such. 31 | // Thus this is checking that the deprecated fields exist and aren't marked 32 | // as deprecated. 33 | #![deny(deprecated)] 34 | let _ = allow_deprecation::ResponseData { 35 | current_user: Some(allow_deprecation::AllowDeprecationCurrentUser { 36 | id: Some("abcd".to_owned()), 37 | name: Some("Angela Merkel".to_owned()), 38 | deprecated_with_reason: Some("foo".to_owned()), 39 | deprecated_no_reason: Some("bar".to_owned()), 40 | }), 41 | }; 42 | } 43 | 44 | #[test] 45 | fn deprecation_deny() { 46 | let _ = deny_deprecation::ResponseData { 47 | current_user: Some(deny_deprecation::DenyDeprecationCurrentUser { 48 | id: Some("abcd".to_owned()), 49 | name: Some("Angela Merkel".to_owned()), 50 | // Notice the deprecated fields are not included here. 51 | // If they were generated, not using them would be a compile error. 52 | // Thus this is checking that the depreacted fields are not 53 | // generated under the `deny` scheme. 54 | }), 55 | }; 56 | } 57 | 58 | #[test] 59 | fn deprecation_warn() { 60 | #![allow(deprecated)] 61 | let _ = warn_deprecation::ResponseData { 62 | current_user: Some(warn_deprecation::WarnDeprecationCurrentUser { 63 | id: Some("abcd".to_owned()), 64 | name: Some("Angela Merkel".to_owned()), 65 | deprecated_with_reason: Some("foo".to_owned()), 66 | deprecated_no_reason: Some("bar".to_owned()), 67 | }), 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /graphql_client/tests/deprecation/query.graphql: -------------------------------------------------------------------------------- 1 | query DenyDeprecation { 2 | currentUser { 3 | name 4 | id 5 | deprecatedWithReason 6 | deprecatedNoReason 7 | } 8 | } 9 | 10 | query AllowDeprecation { 11 | currentUser { 12 | name 13 | id 14 | deprecatedWithReason 15 | deprecatedNoReason 16 | } 17 | } 18 | 19 | query WarnDeprecation { 20 | currentUser { 21 | name 22 | id 23 | deprecatedWithReason 24 | deprecatedNoReason 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /graphql_client/tests/deprecation/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: TestQuery 3 | } 4 | 5 | type TestQuery { 6 | currentUser: TestUser 7 | } 8 | 9 | type TestUser { 10 | name: String 11 | id: ID 12 | deprecatedWithReason: String @deprecated(reason: "Because") 13 | deprecatedNoReason: String @deprecated 14 | } 15 | -------------------------------------------------------------------------------- /graphql_client/tests/extern_enums.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | use serde::Deserialize; 3 | 4 | /* 5 | * Enums under test 6 | * 7 | * They rename the fields to use SCREAMING_SNAKE_CASE for deserialization, as it is the standard for GraphQL enums. 8 | */ 9 | #[derive(Deserialize, Debug, PartialEq, Eq)] 10 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 11 | pub enum Direction { 12 | North, 13 | East, 14 | South, 15 | West, 16 | } 17 | 18 | #[derive(Deserialize, Debug, PartialEq, Eq)] 19 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 20 | pub enum DistanceUnit { 21 | Meter, 22 | Feet, 23 | SomethingElseWithMultipleWords, 24 | } 25 | 26 | /* Queries */ 27 | 28 | // Minimal setup using extern enum. 29 | #[derive(GraphQLQuery)] 30 | #[graphql( 31 | schema_path = "tests/extern_enums/schema.graphql", 32 | query_path = "tests/extern_enums/single_extern_enum_query.graphql", 33 | extern_enums("DistanceUnit") 34 | )] 35 | pub struct SingleExternEnumQuery; 36 | 37 | // Tests using multiple externally defined enums. Also covers mixing with derived traits and with nullable GraphQL enum values. 38 | #[derive(GraphQLQuery)] 39 | #[graphql( 40 | schema_path = "tests/extern_enums/schema.graphql", 41 | query_path = "tests/extern_enums/multiple_extern_enums_query.graphql", 42 | response_derives = "Debug, PartialEq, Eq", 43 | extern_enums("Direction", "DistanceUnit") 44 | )] 45 | pub struct MultipleExternEnumsQuery; 46 | 47 | /* Tests */ 48 | 49 | #[test] 50 | fn single_extern_enum() { 51 | const RESPONSE: &str = include_str!("extern_enums/single_extern_enum_response.json"); 52 | 53 | println!("{:?}", RESPONSE); 54 | let response_data: single_extern_enum_query::ResponseData = 55 | serde_json::from_str(RESPONSE).unwrap(); 56 | 57 | println!("{:?}", response_data.unit); 58 | 59 | let expected = single_extern_enum_query::ResponseData { 60 | unit: DistanceUnit::Meter, 61 | }; 62 | 63 | assert_eq!(response_data.unit, expected.unit); 64 | } 65 | 66 | #[test] 67 | fn multiple_extern_enums() { 68 | const RESPONSE: &str = include_str!("extern_enums/multiple_extern_enums_response.json"); 69 | 70 | println!("{:?}", RESPONSE); 71 | let response_data: multiple_extern_enums_query::ResponseData = 72 | serde_json::from_str(RESPONSE).unwrap(); 73 | 74 | println!("{:?}", response_data); 75 | 76 | let expected = multiple_extern_enums_query::ResponseData { 77 | distance: 100, 78 | direction: Some(Direction::North), 79 | unit: DistanceUnit::SomethingElseWithMultipleWords, 80 | }; 81 | 82 | assert_eq!(response_data, expected); 83 | } 84 | -------------------------------------------------------------------------------- /graphql_client/tests/extern_enums/multiple_extern_enums_query.graphql: -------------------------------------------------------------------------------- 1 | query MultipleExternEnumsQuery { 2 | distance 3 | unit 4 | direction 5 | } 6 | -------------------------------------------------------------------------------- /graphql_client/tests/extern_enums/multiple_extern_enums_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "distance": 100, 3 | "unit": "SOMETHING_ELSE_WITH_MULTIPLE_WORDS", 4 | "direction": "NORTH" 5 | } 6 | -------------------------------------------------------------------------------- /graphql_client/tests/extern_enums/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: ExternEnumQueryRoot 3 | } 4 | 5 | enum Direction { 6 | NORTH 7 | EAST 8 | SOUTH 9 | WEST 10 | } 11 | 12 | enum DistanceUnit { 13 | METER 14 | FEET 15 | SOMETHING_ELSE_WITH_MULTIPLE_WORDS 16 | } 17 | 18 | type ExternEnumQueryRoot { 19 | distance: Int! 20 | unit: DistanceUnit! 21 | direction: Direction 22 | } 23 | -------------------------------------------------------------------------------- /graphql_client/tests/extern_enums/single_extern_enum_query.graphql: -------------------------------------------------------------------------------- 1 | query SingleExternEnumQuery { 2 | unit 3 | } 4 | -------------------------------------------------------------------------------- /graphql_client/tests/extern_enums/single_extern_enum_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "unit": "METER" 3 | } 4 | -------------------------------------------------------------------------------- /graphql_client/tests/fragment_chain.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | 3 | #[allow(dead_code)] 4 | #[derive(GraphQLQuery)] 5 | #[graphql( 6 | schema_path = "tests/fragment_chain/schema.graphql", 7 | query_path = "tests/fragment_chain/query.graphql" 8 | )] 9 | struct Q; 10 | -------------------------------------------------------------------------------- /graphql_client/tests/fragment_chain/query.graphql: -------------------------------------------------------------------------------- 1 | query Q { 2 | ...FragmentB 3 | } 4 | 5 | fragment FragmentB on Query { 6 | ...FragmentA 7 | } 8 | 9 | fragment FragmentA on Query { 10 | x 11 | } 12 | -------------------------------------------------------------------------------- /graphql_client/tests/fragment_chain/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | x: String 3 | } 4 | -------------------------------------------------------------------------------- /graphql_client/tests/fragments.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | use serde_json::json; 3 | 4 | #[derive(GraphQLQuery)] 5 | #[graphql( 6 | query_path = "tests/fragments/query.graphql", 7 | schema_path = "tests/fragments/schema.graphql" 8 | )] 9 | pub struct FragmentReference; 10 | 11 | #[derive(GraphQLQuery)] 12 | #[graphql( 13 | query_path = "tests/fragments/query.graphql", 14 | schema_path = "tests/fragments/schema.graphql" 15 | )] 16 | pub struct SnakeCaseFragment; 17 | 18 | #[test] 19 | fn fragment_reference() { 20 | let valid_response = json!({ 21 | "inFragment": "value", 22 | }); 23 | 24 | let valid_fragment_reference = 25 | serde_json::from_value::(valid_response).unwrap(); 26 | 27 | assert_eq!(valid_fragment_reference.in_fragment.unwrap(), "value"); 28 | } 29 | 30 | #[test] 31 | fn fragments_with_snake_case_name() { 32 | let valid_response = json!({ 33 | "inFragment": "value", 34 | }); 35 | 36 | let valid_fragment_reference = 37 | serde_json::from_value::(valid_response).unwrap(); 38 | 39 | assert_eq!(valid_fragment_reference.in_fragment.unwrap(), "value"); 40 | } 41 | 42 | #[derive(GraphQLQuery)] 43 | #[graphql( 44 | query_path = "tests/fragments/query.graphql", 45 | schema_path = "tests/fragments/schema.graphql" 46 | )] 47 | pub struct RecursiveFragmentQuery; 48 | 49 | #[test] 50 | fn recursive_fragment() { 51 | use recursive_fragment_query::*; 52 | 53 | let _ = RecursiveFragment { 54 | head: Some("ABCD".to_string()), 55 | tail: Some(Box::new(RecursiveFragment { 56 | head: Some("EFGH".to_string()), 57 | tail: None, 58 | })), 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /graphql_client/tests/fragments/query.graphql: -------------------------------------------------------------------------------- 1 | fragment FragmentReference on QueryRoot { 2 | inFragment 3 | } 4 | 5 | fragment snake_case_fragment on QueryRoot { 6 | inFragment 7 | } 8 | 9 | fragment RecursiveFragment on RecursiveNode { 10 | head 11 | tail { 12 | ...RecursiveFragment 13 | } 14 | } 15 | 16 | query FragmentReference { 17 | ...FragmentReference 18 | } 19 | 20 | query SnakeCaseFragment { 21 | ...snake_case_fragment 22 | } 23 | 24 | query RecursiveFragmentQuery { 25 | recursive { 26 | ...RecursiveFragment 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /graphql_client/tests/fragments/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: QueryRoot 3 | } 4 | 5 | type RecursiveNode { 6 | head: String 7 | tail: RecursiveNode 8 | } 9 | 10 | type QueryRoot { 11 | extra: String 12 | inFragment: String 13 | recursive: RecursiveNode! 14 | } 15 | -------------------------------------------------------------------------------- /graphql_client/tests/input_object_variables.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | 3 | #[derive(GraphQLQuery)] 4 | #[graphql( 5 | query_path = "tests/input_object_variables/input_object_variables_query.graphql", 6 | schema_path = "tests/input_object_variables/input_object_variables_schema.graphql", 7 | response_derives = "Debug" 8 | )] 9 | pub struct InputObjectVariablesQuery; 10 | 11 | #[test] 12 | fn input_object_variables_query_variables_struct() { 13 | let _ = input_object_variables_query::Variables { 14 | msg: Some(input_object_variables_query::Message { 15 | content: None, 16 | to: Some(input_object_variables_query::Recipient { 17 | email: "sarah.connor@example.com".to_string(), 18 | category: None, 19 | name: Some("Sarah Connor".to_string()), 20 | }), 21 | }), 22 | }; 23 | } 24 | 25 | // Custom scalars 26 | type Email = String; 27 | 28 | #[derive(GraphQLQuery)] 29 | #[graphql( 30 | query_path = "tests/input_object_variables/input_object_variables_query_defaults.graphql", 31 | schema_path = "tests/input_object_variables/input_object_variables_schema.graphql", 32 | response_derives = "Debug, PartialEq, Eq" 33 | )] 34 | pub struct DefaultInputObjectVariablesQuery; 35 | 36 | #[test] 37 | fn input_object_variables_default() { 38 | let variables = default_input_object_variables_query::Variables { 39 | msg: default_input_object_variables_query::Variables::default_msg(), 40 | }; 41 | 42 | let out = serde_json::to_value(variables).unwrap(); 43 | 44 | let expected_default = serde_json::json!({ 45 | "msg":{"content":null,"to":{"category":null,"email":"rosa.luxemburg@example.com","name":null}} 46 | }); 47 | 48 | assert_eq!(out, expected_default); 49 | } 50 | 51 | #[derive(GraphQLQuery)] 52 | #[graphql( 53 | query_path = "tests/input_object_variables/input_object_variables_query.graphql", 54 | schema_path = "tests/input_object_variables/input_object_variables_schema.graphql", 55 | response_derives = "Debug, PartialEq, Eq" 56 | )] 57 | pub struct RecursiveInputQuery; 58 | 59 | #[test] 60 | fn recursive_input_objects_can_be_constructed() { 61 | use recursive_input_query::*; 62 | 63 | let _ = RecursiveInput { 64 | head: "hello".to_string(), 65 | tail: Box::new(None), 66 | }; 67 | 68 | let _ = RecursiveInput { 69 | head: "hi".to_string(), 70 | tail: Box::new(Some(RecursiveInput { 71 | head: "this is crazy".to_string(), 72 | tail: Box::new(None), 73 | })), 74 | }; 75 | } 76 | 77 | #[derive(GraphQLQuery)] 78 | #[graphql( 79 | query_path = "tests/input_object_variables/input_object_variables_query.graphql", 80 | schema_path = "tests/input_object_variables/input_object_variables_schema.graphql", 81 | response_derives = "Debug, PartialEq, Eq" 82 | )] 83 | pub struct InputCaseTestsQuery; 84 | 85 | #[test] 86 | fn input_objects_are_all_snake_case() { 87 | use input_case_tests_query::*; 88 | 89 | let _ = CaseTestInput { 90 | field_with_snake_case: "hello from".to_string(), 91 | other_field_with_camel_case: "the other side".to_string(), 92 | }; 93 | } 94 | 95 | #[derive(GraphQLQuery)] 96 | #[graphql( 97 | query_path = "tests/input_object_variables/input_object_variables_query.graphql", 98 | schema_path = "tests/input_object_variables/input_object_variables_schema.graphql", 99 | response_derives = "Debug, PartialEq, Eq" 100 | )] 101 | pub struct IndirectlyRecursiveInputQuery; 102 | 103 | #[test] 104 | fn indirectly_recursive_input_objects_can_be_constructed() { 105 | use indirectly_recursive_input_query::*; 106 | 107 | let _ = IndirectlyRecursiveInput { 108 | head: "hello".to_string(), 109 | tail: Box::new(None), 110 | }; 111 | 112 | let _ = IndirectlyRecursiveInput { 113 | head: "hi".to_string(), 114 | tail: Box::new(Some(IndirectlyRecursiveInputTailPart { 115 | name: "this is crazy".to_string(), 116 | recursed_field: Box::new(None), 117 | })), 118 | }; 119 | } 120 | 121 | #[derive(GraphQLQuery)] 122 | #[graphql( 123 | query_path = "tests/input_object_variables/input_object_variables_query.graphql", 124 | schema_path = "tests/input_object_variables/input_object_variables_schema.graphql", 125 | variables_derives = "Default", 126 | response_derives = "Debug, PartialEq, Eq" 127 | )] 128 | pub struct RustNameQuery; 129 | 130 | #[test] 131 | fn rust_name_correctly_mapped() { 132 | use rust_name_query::*; 133 | let value = serde_json::to_value(Variables { 134 | extern_: Some("hello".to_owned()), 135 | msg: <_>::default(), 136 | }) 137 | .unwrap(); 138 | assert_eq!( 139 | value 140 | .as_object() 141 | .unwrap() 142 | .get("extern") 143 | .unwrap() 144 | .as_str() 145 | .unwrap(), 146 | "hello" 147 | ); 148 | } 149 | -------------------------------------------------------------------------------- /graphql_client/tests/input_object_variables/input_object_variables_query.graphql: -------------------------------------------------------------------------------- 1 | query InputObjectVariablesQuery($msg: Message) { 2 | echo(message: $msg) { 3 | result 4 | } 5 | } 6 | 7 | query RecursiveInputQuery($input: RecursiveInput!) { 8 | saveRecursiveInput(recursiveInput: $input) 9 | } 10 | 11 | query IndirectlyRecursiveInputQuery($input: IndirectlyRecursiveInput!) { 12 | saveRecursiveInput(recursiveInput: $input) 13 | } 14 | 15 | query InputCaseTestsQuery($input: CaseTestInput!) { 16 | testQueryCase(caseTestInput: $input) 17 | } 18 | 19 | query RustNameQuery($msg: Message, $extern: String) { 20 | echo(message: $msg, extern: $extern) { 21 | result 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /graphql_client/tests/input_object_variables/input_object_variables_query_defaults.graphql: -------------------------------------------------------------------------------- 1 | query DefaultInputObjectVariablesQuery( 2 | $msg: Message = { to: { email: "rosa.luxemburg@example.com" } } 3 | ) { 4 | echo(message: $msg) { 5 | result 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /graphql_client/tests/input_object_variables/input_object_variables_schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: InputObjectVariablesQuery 3 | } 4 | 5 | scalar Email 6 | 7 | enum Category { 8 | PROFESSIONAL 9 | PERSONAL 10 | } 11 | 12 | input Recipient { 13 | email: Email! 14 | name: String 15 | category: Category 16 | } 17 | 18 | input Message { 19 | to: Recipient 20 | content: String 21 | } 22 | 23 | input Options { 24 | pgpSignature: Boolean 25 | } 26 | 27 | input RecursiveInput { 28 | head: String! 29 | tail: RecursiveInput 30 | } 31 | 32 | input IndirectlyRecursiveInput { 33 | head: String! 34 | tail: IndirectlyRecursiveInputTailPart 35 | } 36 | 37 | input IndirectlyRecursiveInputTailPart { 38 | name: String! 39 | recursed_field: IndirectlyRecursiveInput 40 | } 41 | 42 | input CaseTestInput { 43 | field_with_snake_case: String! 44 | otherFieldWithCamelCase: String! 45 | } 46 | 47 | type CaseTestResult { 48 | result: String! 49 | } 50 | 51 | type InputObjectVariablesQuery { 52 | echo( 53 | message: Message! 54 | options: Options = { pgpSignature: true } 55 | extern: String = "" 56 | ): EchoResult 57 | testQueryCase(caseTestInput: CaseTestInput!): CaseTestResult 58 | saveRecursiveInput(recursiveInput: RecursiveInput!): Category 59 | } 60 | 61 | type EchoResult { 62 | result: String! 63 | } 64 | -------------------------------------------------------------------------------- /graphql_client/tests/int_id.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | use serde_json::json; 3 | 4 | #[derive(GraphQLQuery)] 5 | #[graphql( 6 | schema_path = "tests/more_derives/schema.graphql", 7 | query_path = "tests/more_derives/query.graphql", 8 | response_derives = "Debug, PartialEq, Eq, std::cmp::PartialOrd" 9 | )] 10 | pub struct MoreDerives; 11 | 12 | #[test] 13 | fn int_id() { 14 | let response1 = json!({ 15 | "currentUser": { 16 | "id": 1, 17 | "name": "Don Draper", 18 | } 19 | }); 20 | 21 | let response2 = json!({ 22 | "currentUser": { 23 | "id": "2", 24 | "name": "Peggy Olson", 25 | } 26 | }); 27 | 28 | let res1 = serde_json::from_value::(response1) 29 | .expect("should deserialize"); 30 | assert_eq!( 31 | res1.current_user.expect("res1 current user").id, 32 | Some("1".into()) 33 | ); 34 | 35 | let res2 = serde_json::from_value::(response2) 36 | .expect("should deserialize"); 37 | assert_eq!( 38 | res2.current_user.expect("res2 current user").id, 39 | Some("2".into()) 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /graphql_client/tests/int_id/query.graphql: -------------------------------------------------------------------------------- 1 | query MoreDerives { 2 | currentUser { 3 | name 4 | id 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /graphql_client/tests/int_id/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: TestQuery 3 | } 4 | 5 | type TestQuery { 6 | currentUser: TestUser 7 | } 8 | 9 | type TestUser { 10 | name: String 11 | id: ID 12 | } 13 | -------------------------------------------------------------------------------- /graphql_client/tests/interfaces.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | 3 | const RESPONSE: &str = include_str!("interfaces/interface_response.json"); 4 | 5 | #[derive(GraphQLQuery)] 6 | #[graphql( 7 | query_path = "tests/interfaces/interface_query.graphql", 8 | schema_path = "tests/interfaces/interface_schema.graphql", 9 | response_derives = "Debug, PartialEq, Eq" 10 | )] 11 | pub struct InterfaceQuery; 12 | 13 | #[test] 14 | fn interface_deserialization() { 15 | use interface_query::*; 16 | 17 | println!("{:?}", RESPONSE); 18 | let response_data: interface_query::ResponseData = serde_json::from_str(RESPONSE).unwrap(); 19 | 20 | println!("{:?}", response_data); 21 | 22 | let expected = ResponseData { 23 | everything: Some(vec![ 24 | InterfaceQueryEverything { 25 | name: "Audrey Lorde".to_string(), 26 | on: InterfaceQueryEverythingOn::Person(InterfaceQueryEverythingOnPerson { 27 | birthday: Some("1934-02-18".to_string()), 28 | }), 29 | }, 30 | InterfaceQueryEverything { 31 | name: "Laïka".to_string(), 32 | on: InterfaceQueryEverythingOn::Dog(InterfaceQueryEverythingOnDog { 33 | is_good_dog: true, 34 | }), 35 | }, 36 | InterfaceQueryEverything { 37 | name: "Mozilla".to_string(), 38 | on: InterfaceQueryEverythingOn::Organization( 39 | InterfaceQueryEverythingOnOrganization { 40 | industry: Industry::OTHER, 41 | }, 42 | ), 43 | }, 44 | InterfaceQueryEverything { 45 | name: "Norbert".to_string(), 46 | on: InterfaceQueryEverythingOn::Dog(InterfaceQueryEverythingOnDog { 47 | is_good_dog: true, 48 | }), 49 | }, 50 | ]), 51 | }; 52 | 53 | assert_eq!(response_data, expected); 54 | } 55 | 56 | #[derive(GraphQLQuery)] 57 | #[graphql( 58 | query_path = "tests/interfaces/interface_not_on_everything_query.graphql", 59 | schema_path = "tests/interfaces/interface_schema.graphql", 60 | response_derives = "Debug, PartialEq, Eq" 61 | )] 62 | pub struct InterfaceNotOnEverythingQuery; 63 | 64 | const RESPONSE_NOT_ON_EVERYTHING: &str = 65 | include_str!("interfaces/interface_response_not_on_everything.json"); 66 | 67 | #[test] 68 | fn interface_not_on_everything_deserialization() { 69 | use interface_not_on_everything_query::*; 70 | 71 | let response_data: interface_not_on_everything_query::ResponseData = 72 | serde_json::from_str(RESPONSE_NOT_ON_EVERYTHING).unwrap(); 73 | 74 | let expected = ResponseData { 75 | everything: Some(vec![ 76 | InterfaceNotOnEverythingQueryEverything { 77 | name: "Audrey Lorde".to_string(), 78 | on: InterfaceNotOnEverythingQueryEverythingOn::Person( 79 | InterfaceNotOnEverythingQueryEverythingOnPerson { 80 | birthday: Some("1934-02-18".to_string()), 81 | }, 82 | ), 83 | }, 84 | InterfaceNotOnEverythingQueryEverything { 85 | name: "Laïka".to_string(), 86 | on: InterfaceNotOnEverythingQueryEverythingOn::Dog, 87 | }, 88 | InterfaceNotOnEverythingQueryEverything { 89 | name: "Mozilla".to_string(), 90 | on: InterfaceNotOnEverythingQueryEverythingOn::Organization( 91 | InterfaceNotOnEverythingQueryEverythingOnOrganization { 92 | industry: Industry::OTHER, 93 | }, 94 | ), 95 | }, 96 | InterfaceNotOnEverythingQueryEverything { 97 | name: "Norbert".to_string(), 98 | on: InterfaceNotOnEverythingQueryEverythingOn::Dog, 99 | }, 100 | ]), 101 | }; 102 | 103 | // let expected = r##"ResponseData { everything: Some([InterfaceQueryEverything { name: "Audrey Lorde", on: Person(InterfaceQueryEverythingOnPerson { birthday: Some("1934-02-18") }) }, InterfaceQueryEverything { name: "Laïka", on: Dog }, InterfaceQueryEverything { name: "Mozilla", on: Organization(InterfaceQueryEverythingOnOrganization { industry: OTHER }) }, InterfaceQueryEverything { name: "Norbert", on: Dog }]) }"##; 104 | 105 | assert_eq!(response_data, expected); 106 | 107 | assert_eq!(response_data.everything.map(|names| names.len()), Some(4)); 108 | } 109 | 110 | #[derive(GraphQLQuery)] 111 | #[graphql( 112 | query_path = "tests/interfaces/interface_with_fragment_query.graphql", 113 | schema_path = "tests/interfaces/interface_schema.graphql", 114 | response_derives = "Debug, PartialEq, Eq" 115 | )] 116 | pub struct InterfaceWithFragmentQuery; 117 | 118 | const RESPONSE_FRAGMENT: &str = include_str!("interfaces/interface_with_fragment_response.json"); 119 | 120 | #[test] 121 | fn fragment_in_interface() { 122 | use interface_with_fragment_query::*; 123 | let response_data: ResponseData = 124 | serde_json::from_str(RESPONSE_FRAGMENT).expect("RESPONSE_FRAGMENT did not deserialize"); 125 | 126 | assert_eq!( 127 | response_data, 128 | ResponseData { 129 | everything: Some(vec![ 130 | InterfaceWithFragmentQueryEverything { 131 | name: "Audrey Lorde".to_string(), 132 | public_status: PublicStatus { 133 | display_name: false, 134 | on: PublicStatusOn::Person(PublicStatusOnPerson { 135 | birthday: Some("1934-02-18".to_string()), 136 | age: Some(84), 137 | }), 138 | }, 139 | on: InterfaceWithFragmentQueryEverythingOn::Person( 140 | InterfaceWithFragmentQueryEverythingOnPerson { 141 | birthday: Some("1934-02-18".to_string()), 142 | } 143 | ) 144 | }, 145 | InterfaceWithFragmentQueryEverything { 146 | name: "Laïka".to_string(), 147 | public_status: PublicStatus { 148 | display_name: true, 149 | on: PublicStatusOn::Dog 150 | }, 151 | on: InterfaceWithFragmentQueryEverythingOn::Dog( 152 | InterfaceWithFragmentQueryEverythingOnDog { is_good_dog: true } 153 | ) 154 | }, 155 | InterfaceWithFragmentQueryEverything { 156 | name: "Mozilla".to_string(), 157 | public_status: PublicStatus { 158 | display_name: false, 159 | on: PublicStatusOn::Organization(PublicStatusOnOrganization { 160 | industry: Industry::CAT_FOOD, 161 | }) 162 | }, 163 | on: InterfaceWithFragmentQueryEverythingOn::Organization, 164 | }, 165 | InterfaceWithFragmentQueryEverything { 166 | name: "Norbert".to_string(), 167 | public_status: PublicStatus { 168 | display_name: true, 169 | on: PublicStatusOn::Dog 170 | }, 171 | on: InterfaceWithFragmentQueryEverythingOn::Dog( 172 | InterfaceWithFragmentQueryEverythingOnDog { is_good_dog: true } 173 | ), 174 | }, 175 | ]) 176 | } 177 | ) 178 | } 179 | -------------------------------------------------------------------------------- /graphql_client/tests/interfaces/interface_not_on_everything_query.graphql: -------------------------------------------------------------------------------- 1 | query InterfaceNotOnEverythingQuery { 2 | everything { 3 | __typename 4 | name 5 | ... on Person { 6 | birthday 7 | } 8 | ... on Organization { 9 | industry 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /graphql_client/tests/interfaces/interface_query.graphql: -------------------------------------------------------------------------------- 1 | query InterfaceQuery { 2 | everything { 3 | __typename 4 | name 5 | ... on Dog { 6 | isGoodDog 7 | } 8 | ... on Person { 9 | birthday 10 | } 11 | ... on Organization { 12 | industry 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /graphql_client/tests/interfaces/interface_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "everything": [ 3 | { 4 | "__typename": "Person", 5 | "name": "Audrey Lorde", 6 | "birthday": "1934-02-18" 7 | }, 8 | { 9 | "__typename": "Dog", 10 | "name": "Laïka", 11 | "isGoodDog": true 12 | }, 13 | { 14 | "__typename": "Organization", 15 | "name": "Mozilla", 16 | "industry": "OTHER" 17 | }, 18 | { 19 | "__typename": "Dog", 20 | "name": "Norbert", 21 | "isGoodDog": true 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /graphql_client/tests/interfaces/interface_response_not_on_everything.json: -------------------------------------------------------------------------------- 1 | { 2 | "everything": [ 3 | { 4 | "__typename": "Person", 5 | "name": "Audrey Lorde", 6 | "birthday": "1934-02-18" 7 | }, 8 | { 9 | "__typename": "Dog", 10 | "name": "Laïka" 11 | }, 12 | { 13 | "__typename": "Organization", 14 | "name": "Mozilla", 15 | "industry": "OTHER" 16 | }, 17 | { 18 | "__typename": "Dog", 19 | "name": "Norbert" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /graphql_client/tests/interfaces/interface_schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: InterfaceQuery 3 | } 4 | 5 | interface Named { 6 | name: String! 7 | displayName: Boolean! 8 | } 9 | 10 | type Person implements Named { 11 | name: String! 12 | birthday: String 13 | age: Int 14 | } 15 | 16 | enum Industry { 17 | CAT_FOOD 18 | CHOCOLATE 19 | OTHER 20 | } 21 | 22 | type Organization implements Named { 23 | name: String 24 | industry: Industry! 25 | createdAt: String 26 | } 27 | 28 | type Dog implements Named { 29 | name: String! 30 | """ 31 | Always returns true 32 | """ 33 | isGoodDog: Boolean! 34 | } 35 | 36 | type InterfaceQuery { 37 | everything: [Named!] 38 | } 39 | -------------------------------------------------------------------------------- /graphql_client/tests/interfaces/interface_with_fragment_query.graphql: -------------------------------------------------------------------------------- 1 | fragment PublicStatus on Named { 2 | __typename 3 | displayName 4 | ... on Person { 5 | age 6 | birthday 7 | } 8 | ... on Organization { 9 | industry 10 | } 11 | } 12 | 13 | query InterfaceWithFragmentQuery { 14 | everything { 15 | __typename 16 | name 17 | ...PublicStatus 18 | ... on Dog { 19 | isGoodDog 20 | } 21 | ... on Person { 22 | birthday 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /graphql_client/tests/interfaces/interface_with_fragment_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "everything": [ 3 | { 4 | "__typename": "Person", 5 | "name": "Audrey Lorde", 6 | "displayName": false, 7 | "birthday": "1934-02-18", 8 | "age": 84 9 | }, 10 | { 11 | "__typename": "Dog", 12 | "name": "Laïka", 13 | "displayName": true, 14 | "isGoodDog": true 15 | }, 16 | { 17 | "__typename": "Organization", 18 | "name": "Mozilla", 19 | "displayName": false, 20 | "industry": "CAT_FOOD" 21 | }, 22 | { 23 | "__typename": "Dog", 24 | "name": "Norbert", 25 | "displayName": true, 26 | "isGoodDog": true 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /graphql_client/tests/interfaces/interface_with_type_refining_fragment_query.graphql: -------------------------------------------------------------------------------- 1 | fragment BirthdayFragment on Person { 2 | birthday 3 | } 4 | 5 | query QueryOnInterface { 6 | everything { 7 | __typename 8 | name 9 | ... on Dog { 10 | isGoodDog 11 | } 12 | ...BirthdayFragment 13 | ... on Organization { 14 | industry 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /graphql_client/tests/introspection.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | 3 | #[derive(GraphQLQuery)] 4 | #[graphql( 5 | query_path = "tests/introspection/introspection_query.graphql", 6 | schema_path = "tests/introspection/introspection_schema.graphql", 7 | response_derives = "Debug, PartialEq, Eq" 8 | )] 9 | pub struct IntrospectionQuery; 10 | 11 | #[test] 12 | fn introspection_schema() {} 13 | 14 | const INTROSPECTION_RESPONSE: &str = include_str!("./introspection/introspection_response.json"); 15 | 16 | #[test] 17 | fn leading_underscores_are_preserved() { 18 | let deserialized: graphql_client::Response = 19 | serde_json::from_str(INTROSPECTION_RESPONSE).unwrap(); 20 | assert!(deserialized.data.is_some()); 21 | assert!(deserialized.data.unwrap().schema.is_some()); 22 | } 23 | -------------------------------------------------------------------------------- /graphql_client/tests/introspection/introspection_query.graphql: -------------------------------------------------------------------------------- 1 | query IntrospectionQuery { 2 | __schema { 3 | queryType { 4 | name 5 | } 6 | mutationType { 7 | name 8 | } 9 | subscriptionType { 10 | name 11 | } 12 | types { 13 | ...FullType 14 | } 15 | directives { 16 | name 17 | description 18 | locations 19 | args { 20 | ...InputValue 21 | } 22 | } 23 | } 24 | } 25 | 26 | fragment FullType on __Type { 27 | kind 28 | name 29 | description 30 | fields(includeDeprecated: true) { 31 | name 32 | description 33 | args { 34 | ...InputValue 35 | } 36 | type { 37 | ...TypeRef 38 | } 39 | isDeprecated 40 | deprecationReason 41 | } 42 | inputFields { 43 | ...InputValue 44 | } 45 | interfaces { 46 | ...TypeRef 47 | } 48 | enumValues(includeDeprecated: true) { 49 | name 50 | description 51 | isDeprecated 52 | deprecationReason 53 | } 54 | possibleTypes { 55 | ...TypeRef 56 | } 57 | } 58 | 59 | fragment InputValue on __InputValue { 60 | name 61 | description 62 | type { 63 | ...TypeRef 64 | } 65 | defaultValue 66 | } 67 | 68 | fragment TypeRef on __Type { 69 | kind 70 | name 71 | ofType { 72 | kind 73 | name 74 | ofType { 75 | kind 76 | name 77 | ofType { 78 | kind 79 | name 80 | ofType { 81 | kind 82 | name 83 | ofType { 84 | kind 85 | name 86 | ofType { 87 | kind 88 | name 89 | ofType { 90 | kind 91 | name 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /graphql_client/tests/introspection/introspection_schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | } 4 | 5 | type Query { 6 | __schema: __Schema 7 | } 8 | 9 | type __Schema { 10 | types: [__Type!]! 11 | queryType: __Type! 12 | mutationType: __Type 13 | subscriptionType: __Type 14 | directives: [__Directive!]! 15 | } 16 | 17 | type __Type { 18 | kind: __TypeKind! 19 | name: String 20 | description: String 21 | 22 | # OBJECT and INTERFACE only 23 | fields(includeDeprecated: Boolean = false): [__Field!] 24 | 25 | # OBJECT only 26 | interfaces: [__Type!] 27 | 28 | # INTERFACE and UNION only 29 | possibleTypes: [__Type!] 30 | 31 | # ENUM only 32 | enumValues(includeDeprecated: Boolean = false): [__EnumValue!] 33 | 34 | # INPUT_OBJECT only 35 | inputFields: [__InputValue!] 36 | 37 | # NON_NULL and LIST only 38 | ofType: __Type 39 | } 40 | 41 | type __Field { 42 | name: String! 43 | description: String 44 | args: [__InputValue!]! 45 | type: __Type! 46 | isDeprecated: Boolean! 47 | deprecationReason: String 48 | } 49 | 50 | type __InputValue { 51 | name: String! 52 | description: String 53 | type: __Type! 54 | defaultValue: String 55 | } 56 | 57 | type __EnumValue { 58 | name: String! 59 | description: String 60 | isDeprecated: Boolean! 61 | deprecationReason: String 62 | } 63 | 64 | enum __TypeKind { 65 | SCALAR 66 | OBJECT 67 | INTERFACE 68 | UNION 69 | ENUM 70 | INPUT_OBJECT 71 | LIST 72 | NON_NULL 73 | } 74 | 75 | type __Directive { 76 | name: String! 77 | description: String 78 | locations: [__DirectiveLocation!]! 79 | args: [__InputValue!]! 80 | } 81 | 82 | enum __DirectiveLocation { 83 | QUERY 84 | MUTATION 85 | SUBSCRIPTION 86 | FIELD 87 | FRAGMENT_DEFINITION 88 | FRAGMENT_SPREAD 89 | INLINE_FRAGMENT 90 | SCHEMA 91 | SCALAR 92 | OBJECT 93 | FIELD_DEFINITION 94 | ARGUMENT_DEFINITION 95 | INTERFACE 96 | UNION 97 | ENUM 98 | ENUM_VALUE 99 | INPUT_OBJECT 100 | INPUT_FIELD_DEFINITION 101 | } 102 | -------------------------------------------------------------------------------- /graphql_client/tests/json_schema.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | use serde_json::json; 3 | 4 | type Uuid = String; 5 | 6 | #[derive(GraphQLQuery)] 7 | #[graphql( 8 | query_path = "tests/json_schema/query.graphql", 9 | schema_path = "tests/json_schema/schema_1.json", 10 | response_derives = "Debug, PartialEq, Eq" 11 | )] 12 | pub struct WithSchema1; 13 | 14 | #[derive(GraphQLQuery)] 15 | #[graphql( 16 | query_path = "tests/json_schema/query_2.graphql", 17 | schema_path = "tests/json_schema/schema_2.json", 18 | response_derives = "Debug" 19 | )] 20 | pub struct WithSchema2; 21 | 22 | #[test] 23 | fn json_schemas_work_with_and_without_data_field() { 24 | let response = json!({ 25 | "data": { 26 | "currentSession": null, 27 | }, 28 | }); 29 | 30 | let schema_1_result: graphql_client::Response = 31 | serde_json::from_value(response.clone()).unwrap(); 32 | let schema_2_result: graphql_client::Response = 33 | serde_json::from_value(response).unwrap(); 34 | 35 | assert_eq!( 36 | format!("{:?}", schema_1_result), 37 | format!("{:?}", schema_2_result) 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /graphql_client/tests/json_schema/query.graphql: -------------------------------------------------------------------------------- 1 | query WithSchema1 { 2 | currentSession { 3 | accountId 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /graphql_client/tests/json_schema/query_2.graphql: -------------------------------------------------------------------------------- 1 | query WithSchema2 { 2 | currentSession { 3 | accountId 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /graphql_client/tests/more_derives.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | 3 | #[derive(GraphQLQuery)] 4 | #[graphql( 5 | schema_path = "tests/more_derives/schema.graphql", 6 | query_path = "tests/more_derives/query.graphql", 7 | response_derives = "Debug, PartialEq, Eq, std::cmp::PartialOrd" 8 | )] 9 | pub struct MoreDerives; 10 | 11 | #[test] 12 | fn response_derives_can_be_added() { 13 | let response_data = more_derives::ResponseData { 14 | current_user: Some(more_derives::MoreDerivesCurrentUser { 15 | id: Some("abcd".to_owned()), 16 | name: Some("Angela Merkel".to_owned()), 17 | }), 18 | }; 19 | 20 | let response_data_2 = more_derives::ResponseData { 21 | current_user: Some(more_derives::MoreDerivesCurrentUser { 22 | id: Some("ffff".to_owned()), 23 | name: Some("Winnie the Pooh".to_owned()), 24 | }), 25 | }; 26 | 27 | assert_ne!(response_data, response_data_2); 28 | assert!(response_data < response_data_2); 29 | } 30 | -------------------------------------------------------------------------------- /graphql_client/tests/more_derives/query.graphql: -------------------------------------------------------------------------------- 1 | query MoreDerives { 2 | currentUser { 3 | name 4 | id 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /graphql_client/tests/more_derives/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: TestQuery 3 | } 4 | 5 | type TestQuery { 6 | currentUser: TestUser 7 | } 8 | 9 | type TestUser { 10 | name: String 11 | id: ID 12 | } 13 | -------------------------------------------------------------------------------- /graphql_client/tests/one_of_input.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | use serde_json::*; 3 | 4 | #[derive(GraphQLQuery)] 5 | #[graphql( 6 | schema_path = "tests/one_of_input/schema.graphql", 7 | query_path = "tests/one_of_input/query.graphql", 8 | variables_derives = "Clone" 9 | )] 10 | pub struct OneOfMutation; 11 | 12 | #[test] 13 | fn one_of_input() { 14 | use one_of_mutation::*; 15 | 16 | let author = Param::Author(Author { id: 1 }); 17 | let _ = Param::Name("Mark Twain".to_string()); 18 | let _ = Param::RecursiveDirect(Box::new(author.clone())); 19 | let _ = Param::RecursiveIndirect(Box::new(Recursive { 20 | param: Box::new(author.clone()), 21 | })); 22 | let _ = Param::RequiredInts(vec![1]); 23 | let _ = Param::OptionalInts(vec![Some(1)]); 24 | 25 | let query = OneOfMutation::build_query(Variables { param: author }); 26 | assert_eq!( 27 | json!({ "param": { "author":{ "id": 1 } } }), 28 | serde_json::to_value(&query.variables).expect("json"), 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /graphql_client/tests/one_of_input/query.graphql: -------------------------------------------------------------------------------- 1 | mutation OneOfMutation($param: Param!) { 2 | oneOfMutation(query: $param) 3 | } 4 | -------------------------------------------------------------------------------- /graphql_client/tests/one_of_input/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | mutation: Mutation 3 | } 4 | 5 | type Mutation { 6 | oneOfMutation(mutation: Param!): Int 7 | } 8 | 9 | input Param @oneOf { 10 | author: Author 11 | name: String 12 | recursiveDirect: Param 13 | recursiveIndirect: Recursive 14 | requiredInts: [Int!] 15 | optionalInts: [Int] 16 | } 17 | 18 | input Author { 19 | id: Int! 20 | } 21 | 22 | input Recursive { 23 | param: Param! 24 | } 25 | -------------------------------------------------------------------------------- /graphql_client/tests/operation_selection.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::GraphQLQuery; 2 | 3 | #[derive(GraphQLQuery)] 4 | #[graphql( 5 | query_path = "tests/operation_selection/queries.graphql", 6 | schema_path = "tests/operation_selection/schema.graphql", 7 | response_derives = "Debug, PartialEq, Eq" 8 | )] 9 | pub struct Heights; 10 | 11 | #[derive(GraphQLQuery)] 12 | #[graphql( 13 | query_path = "tests/operation_selection/queries.graphql", 14 | schema_path = "tests/operation_selection/schema.graphql", 15 | response_derives = "Debug, PartialEq, Eq" 16 | )] 17 | pub struct Echo; 18 | 19 | const HEIGHTS_RESPONSE: &str = r#"{"mountainHeight": 224, "buildingHeight": 12}"#; 20 | const ECHO_RESPONSE: &str = r#"{"echo": "tiramisù"}"#; 21 | 22 | #[test] 23 | fn operation_selection_works() { 24 | let heights_response_data: heights::ResponseData = 25 | serde_json::from_str(HEIGHTS_RESPONSE).unwrap(); 26 | let echo_response_data: echo::ResponseData = serde_json::from_str(ECHO_RESPONSE).unwrap(); 27 | 28 | let _echo_variables = echo::Variables { 29 | msg: Some("hi".to_string()), 30 | }; 31 | 32 | let _height_variables = heights::Variables { 33 | building_id: "12".to_string(), 34 | mountain_name: Some("canigou".to_string()), 35 | }; 36 | 37 | let expected_echo = echo::ResponseData { 38 | echo: Some("tiramisù".to_string()), 39 | }; 40 | 41 | let expected_heights = heights::ResponseData { 42 | mountain_height: Some(224), 43 | building_height: Some(12), 44 | }; 45 | 46 | assert_eq!(expected_echo, echo_response_data); 47 | assert_eq!(expected_heights, heights_response_data); 48 | } 49 | 50 | #[test] 51 | fn operation_name_is_correct() { 52 | let echo_variables = echo::Variables { 53 | msg: Some("hi".to_string()), 54 | }; 55 | 56 | let height_variables = heights::Variables { 57 | building_id: "12".to_string(), 58 | mountain_name: Some("canigou".to_string()), 59 | }; 60 | 61 | assert_eq!(Echo::build_query(echo_variables).operation_name, "Echo"); 62 | 63 | assert_eq!( 64 | Heights::build_query(height_variables).operation_name, 65 | "Heights" 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /graphql_client/tests/operation_selection/queries.graphql: -------------------------------------------------------------------------------- 1 | query Heights($buildingId: ID!, $mountainName: String) { 2 | mountainHeight(name: $mountainName) 3 | buildingHeight(id: $buildingId) 4 | } 5 | 6 | query Echo($msg: String) { 7 | echo(msg: $msg) 8 | } 9 | -------------------------------------------------------------------------------- /graphql_client/tests/operation_selection/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: QRoot 3 | } 4 | 5 | type QRoot { 6 | mountainHeight(name: String!): Int 7 | buildingHeight(id: Id!): Int 8 | echo(msg: String!): String 9 | } 10 | -------------------------------------------------------------------------------- /graphql_client/tests/scalar_variables.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | 3 | #[derive(GraphQLQuery)] 4 | #[graphql( 5 | query_path = "tests/scalar_variables/scalar_variables_query.graphql", 6 | schema_path = "tests/scalar_variables/scalar_variables_schema.graphql" 7 | )] 8 | pub struct ScalarVariablesQuery; 9 | 10 | #[test] 11 | fn scalar_variables_query_variables_struct() { 12 | let _ = scalar_variables_query::Variables { 13 | msg: "hello".to_string(), 14 | reps: Some(32), 15 | }; 16 | } 17 | 18 | #[derive(GraphQLQuery)] 19 | #[graphql( 20 | query_path = "tests/scalar_variables/scalar_variables_query_defaults.graphql", 21 | schema_path = "tests/scalar_variables/scalar_variables_schema.graphql" 22 | )] 23 | pub struct DefaultScalarVariablesQuery; 24 | 25 | #[test] 26 | fn scalar_variables_default() { 27 | let variables = default_scalar_variables_query::Variables { 28 | msg: default_scalar_variables_query::Variables::default_msg(), 29 | reps: default_scalar_variables_query::Variables::default_reps(), 30 | }; 31 | 32 | let out = serde_json::to_string(&variables).unwrap(); 33 | 34 | assert_eq!(out, r#"{"msg":"o, hai","reps":3}"#); 35 | } 36 | -------------------------------------------------------------------------------- /graphql_client/tests/scalar_variables/scalar_variables_query.graphql: -------------------------------------------------------------------------------- 1 | query ScalarVariablesQuery($msg: String!, $reps: Int) { 2 | echo(message: $msg, repetitions: $reps) { 3 | result 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /graphql_client/tests/scalar_variables/scalar_variables_query_defaults.graphql: -------------------------------------------------------------------------------- 1 | query DefaultScalarVariablesQuery($msg: String = "o, hai", $reps: Int = 3) { 2 | echo(message: $msg, repetitions: $reps) { 3 | result 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /graphql_client/tests/scalar_variables/scalar_variables_schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: ScalarVariablesQuery 3 | } 4 | 5 | type ScalarVariablesQuery { 6 | echo(message: String!, repetitions: Int): EchoResult 7 | } 8 | 9 | type EchoResult { 10 | result: String! 11 | } 12 | -------------------------------------------------------------------------------- /graphql_client/tests/skip_serializing_none.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | 3 | #[derive(GraphQLQuery)] 4 | #[graphql( 5 | schema_path = "tests/skip_serializing_none/schema.graphql", 6 | query_path = "tests/skip_serializing_none/query.graphql", 7 | skip_serializing_none 8 | )] 9 | pub struct SkipSerializingNoneMutation; 10 | 11 | #[test] 12 | fn skip_serializing_none() { 13 | use skip_serializing_none_mutation::*; 14 | 15 | let query = SkipSerializingNoneMutation::build_query(Variables { 16 | optional_int: None, 17 | optional_list: None, 18 | non_optional_int: 1337, 19 | non_optional_list: vec![], 20 | param: Some(Param { 21 | data: Author { 22 | name: "test".to_owned(), 23 | id: None, 24 | }, 25 | }), 26 | }); 27 | 28 | let stringified = serde_json::to_string(&query).expect("SkipSerializingNoneMutation is valid"); 29 | 30 | println!("{}", stringified); 31 | 32 | assert!(stringified.contains(r#""param":{"data":{"name":"test"}}"#)); 33 | assert!(stringified.contains(r#""nonOptionalInt":1337"#)); 34 | assert!(stringified.contains(r#""nonOptionalList":[]"#)); 35 | assert!(!stringified.contains(r#""optionalInt""#)); 36 | assert!(!stringified.contains(r#""optionalList""#)); 37 | 38 | let query = SkipSerializingNoneMutation::build_query(Variables { 39 | optional_int: Some(42), 40 | optional_list: Some(vec![]), 41 | non_optional_int: 1337, 42 | non_optional_list: vec![], 43 | param: Some(Param { 44 | data: Author { 45 | name: "test".to_owned(), 46 | id: None, 47 | }, 48 | }), 49 | }); 50 | let stringified = serde_json::to_string(&query).expect("SkipSerializingNoneMutation is valid"); 51 | println!("{}", stringified); 52 | assert!(stringified.contains(r#""param":{"data":{"name":"test"}}"#)); 53 | assert!(stringified.contains(r#""nonOptionalInt":1337"#)); 54 | assert!(stringified.contains(r#""nonOptionalList":[]"#)); 55 | assert!(stringified.contains(r#""optionalInt":42"#)); 56 | assert!(stringified.contains(r#""optionalList":[]"#)); 57 | } 58 | -------------------------------------------------------------------------------- /graphql_client/tests/skip_serializing_none/query.graphql: -------------------------------------------------------------------------------- 1 | mutation SkipSerializingNoneMutation($param: Param, $optionalInt: Int, $optionalList: [Int!], $nonOptionalInt: Int!, $nonOptionalList: [Int!]!) { 2 | optInput(query: $param) { 3 | name 4 | __typename 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /graphql_client/tests/skip_serializing_none/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | mutation: Mutation 3 | } 4 | 5 | # The query type, represents all of the entry points into our object graph 6 | type Mutation { 7 | optInput(mutation: Param!): Named 8 | } 9 | 10 | input Param { 11 | data: Author! 12 | } 13 | 14 | input Author { 15 | id: String 16 | name: String! 17 | } 18 | 19 | # A named entity 20 | type Named { 21 | # The ID of the entity 22 | id: ID! 23 | # The name of the entity 24 | name: String! 25 | } 26 | -------------------------------------------------------------------------------- /graphql_client/tests/subscription/subscription_invalid_query.graphql: -------------------------------------------------------------------------------- 1 | subscription InvalidSubscription($filter: String) { 2 | newDogs { 3 | name 4 | } 5 | dogBirthdays(filter: $filter) { 6 | name 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /graphql_client/tests/subscription/subscription_query.graphql: -------------------------------------------------------------------------------- 1 | subscription SubscriptionQuery($filter: String) { 2 | dogBirthdays(filter: $filter) { 3 | name 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /graphql_client/tests/subscription/subscription_query_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "dogBirthdays": [ 3 | { 4 | "name": "Maya" 5 | }, 6 | { 7 | "name": "Norbert" 8 | }, 9 | { 10 | "name": "Strelka" 11 | }, 12 | { 13 | "name": "Belka" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /graphql_client/tests/subscription/subscription_schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: SimpleQuery 3 | mutation: SimpleMutation 4 | subscription: SimpleSubscription 5 | } 6 | 7 | type SimpleQuery { 8 | dogByName(name: String): Dog 9 | } 10 | 11 | type SimpleMutation { 12 | petDog(dogName: String): Dog 13 | } 14 | 15 | type SimpleSubscription { 16 | newDogs: [Dog] 17 | dogBirthdays(filter: String): [DogBirthday!] 18 | } 19 | 20 | type DogBirthday { 21 | name: String 22 | date: String 23 | age: Int 24 | treats: [String] 25 | } 26 | 27 | type Dog { 28 | name: String! 29 | """ 30 | Always returns true 31 | """ 32 | isGoodDog: Boolean! 33 | } 34 | -------------------------------------------------------------------------------- /graphql_client/tests/subscriptions.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | 3 | const RESPONSE: &str = include_str!("subscription/subscription_query_response.json"); 4 | 5 | // If you uncomment this, it will not compile because the query is not valid. We need to investigate how we can make this a real test. 6 | // 7 | // #[derive(GraphQLQuery)] 8 | // #[graphql( 9 | // schema_path = "tests/subscription/subscription_schema.graphql", 10 | // query_path = "tests/subscription/subscription_invalid_query.graphql" 11 | // )] 12 | // struct SubscriptionInvalidQuery; 13 | 14 | #[derive(GraphQLQuery)] 15 | #[graphql( 16 | schema_path = "tests/subscription/subscription_schema.graphql", 17 | query_path = "tests/subscription/subscription_query.graphql", 18 | response_derives = "Debug, PartialEq, Eq" 19 | )] 20 | pub struct SubscriptionQuery; 21 | 22 | #[test] 23 | fn subscriptions_work() { 24 | let response_data: subscription_query::ResponseData = serde_json::from_str(RESPONSE).unwrap(); 25 | 26 | let expected = subscription_query::ResponseData { 27 | dog_birthdays: Some(vec![ 28 | subscription_query::SubscriptionQueryDogBirthdays { 29 | name: Some("Maya".to_string()), 30 | }, 31 | subscription_query::SubscriptionQueryDogBirthdays { 32 | name: Some("Norbert".to_string()), 33 | }, 34 | subscription_query::SubscriptionQueryDogBirthdays { 35 | name: Some("Strelka".to_string()), 36 | }, 37 | subscription_query::SubscriptionQueryDogBirthdays { 38 | name: Some("Belka".to_string()), 39 | }, 40 | ]), 41 | }; 42 | 43 | assert_eq!(response_data, expected); 44 | 45 | assert_eq!( 46 | response_data.dog_birthdays.map(|birthdays| birthdays.len()), 47 | Some(4) 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /graphql_client/tests/type_refining_fragments.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | 3 | #[derive(GraphQLQuery)] 4 | #[graphql( 5 | query_path = "tests/interfaces/interface_with_type_refining_fragment_query.graphql", 6 | schema_path = "tests/interfaces/interface_schema.graphql", 7 | response_derives = "Debug, PartialEq, Eq" 8 | )] 9 | pub struct QueryOnInterface; 10 | 11 | #[derive(GraphQLQuery)] 12 | #[graphql( 13 | query_path = "tests/unions/type_refining_fragment_on_union_query.graphql", 14 | schema_path = "tests/unions/union_schema.graphql", 15 | response_derives = "Debug, PartialEq, Eq" 16 | )] 17 | pub struct QueryOnUnion; 18 | 19 | #[test] 20 | fn type_refining_fragment_on_union() { 21 | const RESPONSE: &str = include_str!("unions/union_query_response.json"); 22 | 23 | let response_data: query_on_union::ResponseData = serde_json::from_str(RESPONSE).unwrap(); 24 | 25 | let expected = query_on_union::ResponseData { 26 | names: Some(vec![ 27 | query_on_union::QueryOnUnionNames::Person(query_on_union::QueryOnUnionNamesOnPerson { 28 | first_name: "Audrey".to_string(), 29 | last_name: Some("Lorde".to_string()), 30 | }), 31 | query_on_union::QueryOnUnionNames::Dog(query_on_union::QueryOnUnionNamesOnDog { 32 | name: "Laïka".to_string(), 33 | }), 34 | query_on_union::QueryOnUnionNames::Organization( 35 | query_on_union::QueryOnUnionNamesOnOrganization { 36 | title: "Mozilla".to_string(), 37 | }, 38 | ), 39 | query_on_union::QueryOnUnionNames::Dog(query_on_union::QueryOnUnionNamesOnDog { 40 | name: "Norbert".to_string(), 41 | }), 42 | ]), 43 | }; 44 | 45 | assert_eq!(response_data, expected); 46 | } 47 | 48 | #[test] 49 | fn type_refining_fragment_on_interface() { 50 | use crate::query_on_interface::*; 51 | 52 | const RESPONSE: &str = include_str!("interfaces/interface_response.json"); 53 | 54 | let response_data: query_on_interface::ResponseData = serde_json::from_str(RESPONSE).unwrap(); 55 | 56 | let expected = ResponseData { 57 | everything: Some(vec![ 58 | QueryOnInterfaceEverything { 59 | name: "Audrey Lorde".to_string(), 60 | on: QueryOnInterfaceEverythingOn::Person(QueryOnInterfaceEverythingOnPerson { 61 | birthday: Some("1934-02-18".to_string()), 62 | }), 63 | }, 64 | QueryOnInterfaceEverything { 65 | name: "Laïka".to_string(), 66 | on: QueryOnInterfaceEverythingOn::Dog(QueryOnInterfaceEverythingOnDog { 67 | is_good_dog: true, 68 | }), 69 | }, 70 | QueryOnInterfaceEverything { 71 | name: "Mozilla".to_string(), 72 | on: QueryOnInterfaceEverythingOn::Organization( 73 | QueryOnInterfaceEverythingOnOrganization { 74 | industry: Industry::OTHER, 75 | }, 76 | ), 77 | }, 78 | QueryOnInterfaceEverything { 79 | name: "Norbert".to_string(), 80 | on: QueryOnInterfaceEverythingOn::Dog(QueryOnInterfaceEverythingOnDog { 81 | is_good_dog: true, 82 | }), 83 | }, 84 | ]), 85 | }; 86 | 87 | assert_eq!(response_data, expected); 88 | } 89 | -------------------------------------------------------------------------------- /graphql_client/tests/union_query.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::*; 2 | 3 | const RESPONSE: &str = include_str!("unions/union_query_response.json"); 4 | const FRAGMENT_AND_MORE_RESPONSE: &str = include_str!("unions/fragment_and_more_response.json"); 5 | 6 | #[derive(GraphQLQuery)] 7 | #[graphql( 8 | query_path = "tests/unions/union_query.graphql", 9 | schema_path = "tests/unions/union_schema.graphql", 10 | response_derives = "Debug, PartialEq, Eq" 11 | )] 12 | pub struct UnionQuery; 13 | 14 | #[derive(GraphQLQuery)] 15 | #[graphql( 16 | query_path = "tests/unions/union_query.graphql", 17 | schema_path = "tests/unions/union_schema.graphql", 18 | response_derives = "Debug, PartialEq, Eq" 19 | )] 20 | pub struct FragmentOnUnion; 21 | 22 | #[derive(GraphQLQuery)] 23 | #[graphql( 24 | query_path = "tests/unions/union_query.graphql", 25 | schema_path = "tests/unions/union_schema.graphql", 26 | response_derives = "Debug, PartialEq, Eq" 27 | )] 28 | pub struct FragmentAndMoreOnUnion; 29 | 30 | #[test] 31 | fn union_query_deserialization() { 32 | let response_data: union_query::ResponseData = serde_json::from_str(RESPONSE).unwrap(); 33 | 34 | let expected = union_query::ResponseData { 35 | names: Some(vec![ 36 | union_query::UnionQueryNames::Person(union_query::UnionQueryNamesOnPerson { 37 | first_name: "Audrey".to_string(), 38 | last_name: Some("Lorde".to_string()), 39 | }), 40 | union_query::UnionQueryNames::Dog(union_query::UnionQueryNamesOnDog { 41 | name: "Laïka".to_string(), 42 | }), 43 | union_query::UnionQueryNames::Organization( 44 | union_query::UnionQueryNamesOnOrganization { 45 | title: "Mozilla".to_string(), 46 | }, 47 | ), 48 | union_query::UnionQueryNames::Dog(union_query::UnionQueryNamesOnDog { 49 | name: "Norbert".to_string(), 50 | }), 51 | ]), 52 | }; 53 | 54 | assert_eq!(response_data, expected); 55 | 56 | assert_eq!(response_data.names.map(|names| names.len()), Some(4)); 57 | } 58 | 59 | #[test] 60 | fn fragment_on_union() { 61 | let response_data: fragment_on_union::ResponseData = serde_json::from_str(RESPONSE).unwrap(); 62 | 63 | let expected = fragment_on_union::ResponseData { 64 | names: Some(vec![ 65 | fragment_on_union::NamesFragment::Person(fragment_on_union::NamesFragmentOnPerson { 66 | first_name: "Audrey".to_string(), 67 | }), 68 | fragment_on_union::NamesFragment::Dog(fragment_on_union::NamesFragmentOnDog { 69 | name: "Laïka".to_string(), 70 | }), 71 | fragment_on_union::NamesFragment::Organization( 72 | fragment_on_union::NamesFragmentOnOrganization { 73 | title: "Mozilla".to_string(), 74 | }, 75 | ), 76 | fragment_on_union::NamesFragment::Dog(fragment_on_union::NamesFragmentOnDog { 77 | name: "Norbert".to_string(), 78 | }), 79 | ]), 80 | }; 81 | 82 | assert_eq!(response_data, expected); 83 | } 84 | 85 | #[test] 86 | fn fragment_and_more_on_union() { 87 | use fragment_and_more_on_union::*; 88 | 89 | let response_data: fragment_and_more_on_union::ResponseData = 90 | serde_json::from_str(FRAGMENT_AND_MORE_RESPONSE).unwrap(); 91 | 92 | let expected = fragment_and_more_on_union::ResponseData { 93 | names: Some(vec![ 94 | FragmentAndMoreOnUnionNames { 95 | names_fragment: NamesFragment::Person(NamesFragmentOnPerson { 96 | first_name: "Larry".into(), 97 | }), 98 | on: FragmentAndMoreOnUnionNamesOn::Person, 99 | }, 100 | FragmentAndMoreOnUnionNames { 101 | names_fragment: NamesFragment::Dog(NamesFragmentOnDog { 102 | name: "Laïka".into(), 103 | }), 104 | on: FragmentAndMoreOnUnionNamesOn::Dog(FragmentAndMoreOnUnionNamesOnDog { 105 | is_good_dog: true, 106 | }), 107 | }, 108 | FragmentAndMoreOnUnionNames { 109 | names_fragment: NamesFragment::Organization(NamesFragmentOnOrganization { 110 | title: "Mozilla".into(), 111 | }), 112 | on: FragmentAndMoreOnUnionNamesOn::Organization, 113 | }, 114 | FragmentAndMoreOnUnionNames { 115 | names_fragment: NamesFragment::Dog(NamesFragmentOnDog { 116 | name: "Norbert".into(), 117 | }), 118 | on: FragmentAndMoreOnUnionNamesOn::Dog(FragmentAndMoreOnUnionNamesOnDog { 119 | is_good_dog: true, 120 | }), 121 | }, 122 | ]), 123 | }; 124 | 125 | assert_eq!(response_data, expected); 126 | } 127 | -------------------------------------------------------------------------------- /graphql_client/tests/unions/fragment_and_more_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "names": [ 3 | { 4 | "__typename": "Person", 5 | "firstName": "Larry" 6 | }, 7 | { 8 | "__typename": "Dog", 9 | "name": "Laïka", 10 | "isGoodDog": true 11 | }, 12 | { 13 | "__typename": "Organization", 14 | "title": "Mozilla" 15 | }, 16 | { 17 | "__typename": "Dog", 18 | "name": "Norbert", 19 | "isGoodDog": true 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /graphql_client/tests/unions/type_refining_fragment_on_union_query.graphql: -------------------------------------------------------------------------------- 1 | fragment DogName on Dog { 2 | name 3 | } 4 | 5 | query QueryOnUnion { 6 | names { 7 | __typename 8 | ...DogName 9 | ... on Person { 10 | firstName 11 | lastName 12 | } 13 | ... on Organization { 14 | title 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /graphql_client/tests/unions/union_query.graphql: -------------------------------------------------------------------------------- 1 | query UnionQuery { 2 | names { 3 | __typename 4 | ... on Dog { 5 | name 6 | } 7 | ... on Person { 8 | firstName 9 | lastName 10 | } 11 | ... on Organization { 12 | title 13 | } 14 | } 15 | } 16 | 17 | fragment NamesFragment on NamedThing { 18 | __typename 19 | ... on Dog { 20 | name 21 | } 22 | ... on Person { 23 | firstName 24 | } 25 | ... on Organization { 26 | title 27 | } 28 | } 29 | 30 | query FragmentOnUnion { 31 | names { 32 | ...NamesFragment 33 | } 34 | } 35 | 36 | query FragmentAndMoreOnUnion { 37 | names { 38 | ...NamesFragment 39 | ... on Dog { 40 | isGoodDog 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /graphql_client/tests/unions/union_query_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "names": [ 3 | { 4 | "__typename": "Person", 5 | "firstName": "Audrey", 6 | "lastName": "Lorde" 7 | }, 8 | { 9 | "__typename": "Dog", 10 | "name": "Laïka" 11 | }, 12 | { 13 | "__typename": "Organization", 14 | "title": "Mozilla" 15 | }, 16 | { 17 | "__typename": "Dog", 18 | "name": "Norbert" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /graphql_client/tests/unions/union_schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: UnionQuery 3 | } 4 | 5 | type Person { 6 | firstName: String! 7 | lastName: String 8 | birthday: String 9 | } 10 | 11 | type Organization { 12 | title: String! 13 | createdAt: String 14 | } 15 | 16 | type Dog { 17 | name: String! 18 | """ 19 | Always returns true 20 | """ 21 | isGoodDog: Boolean! 22 | } 23 | 24 | union NamedThing = Person | Dog | Organization 25 | 26 | type UnionQuery { 27 | names: [NamedThing!] 28 | } 29 | -------------------------------------------------------------------------------- /graphql_client_cli/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /graphql_client_cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphql_client_cli" 3 | description = "The CLI for graphql-client" 4 | version = "0.14.0" 5 | authors = ["Tom Houlé "] 6 | license = "Apache-2.0 OR MIT" 7 | repository = "https://github.com/graphql-rust/graphql-client" 8 | edition = "2018" 9 | 10 | [[bin]] 11 | name = "graphql-client" 12 | path = "src/main.rs" 13 | 14 | [dependencies] 15 | reqwest = { version = "0.12", features = ["json", "blocking"] } 16 | graphql_client = { version = "0.14.0", path = "../graphql_client", default-features = false, features = ["graphql_query_derive", "reqwest-blocking"] } 17 | graphql_client_codegen = { path = "../graphql_client_codegen/", version = "0.14.0" } 18 | clap = { version = "^4.0", features = ["derive"] } 19 | serde = { version = "^1.0", features = ["derive"] } 20 | serde_json = "^1.0" 21 | log = "^0.4" 22 | env_logger = { version = "0.10.2", features = ["color"] } 23 | syn = { version = "^2.0", features = ["full"] } 24 | anstyle = "1.0.10" 25 | 26 | [features] 27 | default = [] 28 | -------------------------------------------------------------------------------- /graphql_client_cli/README.md: -------------------------------------------------------------------------------- 1 | # GraphQL client CLI 2 | 3 | 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). 4 | 5 | ## Install 6 | 7 | ```bash 8 | cargo install graphql_client_cli --force 9 | ``` 10 | 11 | ## introspect schema 12 | 13 | ``` 14 | Get the schema from a live GraphQL API. The schema is printed to stdout. 15 | 16 | USAGE: 17 | graphql-client introspect-schema [FLAGS] [OPTIONS] 18 | 19 | FLAGS: 20 | -h, --help Prints help information 21 | -V, --version Prints version information 22 | --no-ssl Set this option to disable ssl certificate verification. Default value is false. 23 | ssl verification is turned on by default. 24 | 25 | OPTIONS: 26 | --authorization Set the contents of the Authorization header. 27 | --header ... Specify custom headers. --header 'X-Name: Value' 28 | --output Where to write the JSON for the introspected schema. 29 | 30 | ARGS: 31 | The URL of a GraphQL endpoint to introspect. 32 | ``` 33 | 34 | ## generate client code 35 | 36 | ``` 37 | USAGE: 38 | graphql-client generate [FLAGS] [OPTIONS] --schema-path 39 | 40 | FLAGS: 41 | -h, --help Prints help information 42 | --no-formatting If you don't want to execute rustfmt to generated code, set this option. Default value is 43 | false. 44 | -V, --version Prints version information 45 | 46 | OPTIONS: 47 | -I, --variables-derives 48 | Additional derives that will be added to the generated structs and enums for the variables. 49 | --variables-derives='Serialize,PartialEq' 50 | -O, --response-derives 51 | Additional derives that will be added to the generated structs and enums for the response. 52 | --response-derives='Serialize,PartialEq' 53 | -d, --deprecation-strategy 54 | You can choose deprecation strategy from allow, deny, or warn. Default value is warn. 55 | 56 | -m, --module-visibility 57 | You can choose module and target struct visibility from pub and private. Default value is pub. 58 | 59 | -o, --output-directory The directory in which the code will be generated 60 | -s, --schema-path Path to GraphQL schema file (.json or .graphql). 61 | -o, --selected-operation 62 | Name of target query. If you don't set this parameter, cli generate all queries in query file. 63 | --fragments-other-variant 64 | Generate an Unknown variant for enums generated by fragments. 65 | 66 | 67 | ARGS: 68 | Path to the GraphQL query file. 69 | ``` 70 | 71 | If you want to use formatting feature, you should install like this. 72 | 73 | ```bash 74 | cargo install graphql_client_cli 75 | ``` 76 | -------------------------------------------------------------------------------- /graphql_client_cli/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display}; 2 | 3 | pub struct Error { 4 | source: Option>, 5 | message: Option, 6 | location: &'static std::panic::Location<'static>, 7 | } 8 | 9 | impl Error { 10 | #[track_caller] 11 | pub fn message(msg: String) -> Self { 12 | Error { 13 | source: None, 14 | message: Some(msg), 15 | location: std::panic::Location::caller(), 16 | } 17 | } 18 | 19 | #[track_caller] 20 | pub fn source_with_message( 21 | source: impl std::error::Error + Send + Sync + 'static, 22 | message: String, 23 | ) -> Self { 24 | let mut err = Error::message(message); 25 | err.source = Some(Box::new(source)); 26 | err 27 | } 28 | } 29 | 30 | // This is the impl that shows up when the error bubbles up to `main()`. 31 | impl Debug for Error { 32 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 33 | if let Some(msg) = &self.message { 34 | f.write_str(msg)?; 35 | f.write_str("\n")?; 36 | } 37 | 38 | if self.source.is_some() && self.message.is_some() { 39 | f.write_str("Cause: ")?; 40 | } 41 | 42 | if let Some(source) = self.source.as_ref() { 43 | Display::fmt(source, f)?; 44 | } 45 | 46 | f.write_str("\nLocation: ")?; 47 | Display::fmt(self.location, f)?; 48 | 49 | Ok(()) 50 | } 51 | } 52 | 53 | impl From for Error 54 | where 55 | T: std::error::Error + Send + Sync + 'static, 56 | { 57 | #[track_caller] 58 | fn from(err: T) -> Self { 59 | Error { 60 | message: None, 61 | source: Some(Box::new(err)), 62 | location: std::panic::Location::caller(), 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /graphql_client_cli/src/generate.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use crate::CliResult; 3 | use graphql_client_codegen::{ 4 | generate_module_token_stream, CodegenMode, GraphQLClientCodegenOptions, 5 | }; 6 | use std::ffi::OsString; 7 | use std::fs::File; 8 | use std::io::Write as _; 9 | use std::path::PathBuf; 10 | use std::process::Stdio; 11 | use syn::{token::Paren, token::Pub, VisRestricted, Visibility}; 12 | 13 | pub(crate) struct CliCodegenParams { 14 | pub query_path: PathBuf, 15 | pub schema_path: PathBuf, 16 | pub selected_operation: Option, 17 | pub variables_derives: Option, 18 | pub response_derives: Option, 19 | pub deprecation_strategy: Option, 20 | pub no_formatting: bool, 21 | pub module_visibility: Option, 22 | pub output_directory: Option, 23 | pub custom_scalars_module: Option, 24 | pub fragments_other_variant: bool, 25 | pub external_enums: Option>, 26 | pub custom_variable_types: Option, 27 | pub custom_response_type: Option, 28 | } 29 | 30 | const WARNING_SUPPRESSION: &str = "#![allow(clippy::all, warnings)]"; 31 | 32 | pub(crate) fn generate_code(params: CliCodegenParams) -> CliResult<()> { 33 | let CliCodegenParams { 34 | variables_derives, 35 | response_derives, 36 | deprecation_strategy, 37 | no_formatting, 38 | output_directory, 39 | module_visibility: _module_visibility, 40 | query_path, 41 | schema_path, 42 | selected_operation, 43 | custom_scalars_module, 44 | fragments_other_variant, 45 | external_enums, 46 | custom_variable_types, 47 | custom_response_type, 48 | } = params; 49 | 50 | let deprecation_strategy = deprecation_strategy.as_ref().and_then(|s| s.parse().ok()); 51 | 52 | let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); 53 | 54 | options.set_module_visibility(match _module_visibility { 55 | Some(v) => match v.to_lowercase().as_str() { 56 | "pub" => Visibility::Public(Pub::default()), 57 | "inherited" => Visibility::Inherited, 58 | _ => Visibility::Restricted(VisRestricted { 59 | pub_token: Pub::default(), 60 | in_token: None, 61 | paren_token: Paren::default(), 62 | path: syn::parse_str(&v).unwrap(), 63 | }), 64 | }, 65 | None => Visibility::Public(Pub::default()), 66 | }); 67 | 68 | options.set_fragments_other_variant(fragments_other_variant); 69 | 70 | if let Some(selected_operation) = selected_operation { 71 | options.set_operation_name(selected_operation); 72 | } 73 | 74 | if let Some(variables_derives) = variables_derives { 75 | options.set_variables_derives(variables_derives); 76 | } 77 | 78 | if let Some(response_derives) = response_derives { 79 | options.set_response_derives(response_derives); 80 | } 81 | 82 | if let Some(deprecation_strategy) = deprecation_strategy { 83 | options.set_deprecation_strategy(deprecation_strategy); 84 | } 85 | 86 | if let Some(external_enums) = external_enums { 87 | options.set_extern_enums(external_enums); 88 | } 89 | 90 | if let Some(custom_scalars_module) = custom_scalars_module { 91 | let custom_scalars_module = syn::parse_str(&custom_scalars_module) 92 | .map_err(|_| Error::message("Invalid custom scalar module path".to_owned()))?; 93 | 94 | options.set_custom_scalars_module(custom_scalars_module); 95 | } 96 | 97 | if let Some(custom_variable_types) = custom_variable_types { 98 | options.set_custom_variable_types(custom_variable_types.split(",").map(String::from).collect()); 99 | } 100 | 101 | if let Some(custom_response_type) = custom_response_type { 102 | options.set_custom_response_type(custom_response_type); 103 | } 104 | 105 | let gen = generate_module_token_stream(query_path.clone(), &schema_path, options) 106 | .map_err(|err| Error::message(format!("Error generating module code: {}", err)))?; 107 | 108 | let generated_code = format!("{}\n{}", WARNING_SUPPRESSION, gen); 109 | let generated_code = if !no_formatting { 110 | format(&generated_code)? 111 | } else { 112 | generated_code 113 | }; 114 | 115 | let query_file_name: OsString = 116 | query_path 117 | .file_name() 118 | .map(ToOwned::to_owned) 119 | .ok_or_else(|| { 120 | Error::message("Failed to find a file name in the provided query path.".to_owned()) 121 | })?; 122 | 123 | let dest_file_path: PathBuf = output_directory 124 | .map(|output_dir| output_dir.join(query_file_name).with_extension("rs")) 125 | .unwrap_or_else(move || query_path.with_extension("rs")); 126 | 127 | log::info!("Writing generated query to {:?}", dest_file_path); 128 | 129 | let mut file = File::create(&dest_file_path).map_err(|err| { 130 | Error::source_with_message( 131 | err, 132 | format!("Creating file at {}", dest_file_path.display()), 133 | ) 134 | })?; 135 | write!(file, "{}", generated_code)?; 136 | 137 | Ok(()) 138 | } 139 | 140 | fn format(code: &str) -> CliResult { 141 | let binary = "rustfmt"; 142 | 143 | let mut child = std::process::Command::new(binary) 144 | .stdin(Stdio::piped()) 145 | .stdout(Stdio::piped()) 146 | .spawn() 147 | .map_err(|err| Error::source_with_message(err, "Error spawning rustfmt".to_owned()))?; 148 | let child_stdin = child.stdin.as_mut().unwrap(); 149 | write!(child_stdin, "{}", code)?; 150 | 151 | let output = child.wait_with_output()?; 152 | 153 | if !output.status.success() { 154 | panic!( 155 | "rustfmt error\n\n{}", 156 | String::from_utf8_lossy(&output.stderr) 157 | ); 158 | } 159 | 160 | Ok(String::from_utf8(output.stdout)?) 161 | } 162 | -------------------------------------------------------------------------------- /graphql_client_cli/src/graphql/introspection_query.graphql: -------------------------------------------------------------------------------- 1 | query IntrospectionQuery { 2 | __schema { 3 | queryType { 4 | name 5 | } 6 | mutationType { 7 | name 8 | } 9 | subscriptionType { 10 | name 11 | } 12 | types { 13 | ...FullType 14 | } 15 | directives { 16 | name 17 | description 18 | locations 19 | args { 20 | ...InputValue 21 | } 22 | } 23 | } 24 | } 25 | 26 | fragment FullType on __Type { 27 | kind 28 | name 29 | description 30 | fields(includeDeprecated: true) { 31 | name 32 | description 33 | args { 34 | ...InputValue 35 | } 36 | type { 37 | ...TypeRef 38 | } 39 | isDeprecated 40 | deprecationReason 41 | } 42 | inputFields { 43 | ...InputValue 44 | } 45 | interfaces { 46 | ...TypeRef 47 | } 48 | enumValues(includeDeprecated: true) { 49 | name 50 | description 51 | isDeprecated 52 | deprecationReason 53 | } 54 | possibleTypes { 55 | ...TypeRef 56 | } 57 | } 58 | 59 | fragment InputValue on __InputValue { 60 | name 61 | description 62 | type { 63 | ...TypeRef 64 | } 65 | defaultValue 66 | } 67 | 68 | fragment TypeRef on __Type { 69 | kind 70 | name 71 | ofType { 72 | kind 73 | name 74 | ofType { 75 | kind 76 | name 77 | ofType { 78 | kind 79 | name 80 | ofType { 81 | kind 82 | name 83 | ofType { 84 | kind 85 | name 86 | ofType { 87 | kind 88 | name 89 | ofType { 90 | kind 91 | name 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /graphql_client_cli/src/graphql/introspection_query_with_isOneOf_specifiedByUrl.graphql: -------------------------------------------------------------------------------- 1 | query IntrospectionQueryWithIsOneOfSpecifiedByURL { 2 | __schema { 3 | queryType { 4 | name 5 | } 6 | mutationType { 7 | name 8 | } 9 | subscriptionType { 10 | name 11 | } 12 | types { 13 | ...FullTypeWithisOneOfSpecifiedByURL 14 | } 15 | directives { 16 | name 17 | description 18 | locations 19 | args { 20 | ...InputValue 21 | } 22 | } 23 | } 24 | } 25 | 26 | fragment FullTypeWithisOneOfSpecifiedByURL on __Type { 27 | kind 28 | name 29 | description 30 | isOneOf 31 | specifiedByURL 32 | fields(includeDeprecated: true) { 33 | name 34 | description 35 | args { 36 | ...InputValue 37 | } 38 | type { 39 | ...TypeRef 40 | } 41 | isDeprecated 42 | deprecationReason 43 | } 44 | inputFields { 45 | ...InputValue 46 | } 47 | interfaces { 48 | ...TypeRef 49 | } 50 | enumValues(includeDeprecated: true) { 51 | name 52 | description 53 | isDeprecated 54 | deprecationReason 55 | } 56 | possibleTypes { 57 | ...TypeRef 58 | } 59 | } 60 | 61 | fragment InputValue on __InputValue { 62 | name 63 | description 64 | type { 65 | ...TypeRef 66 | } 67 | defaultValue 68 | } 69 | 70 | fragment TypeRef on __Type { 71 | kind 72 | name 73 | ofType { 74 | kind 75 | name 76 | ofType { 77 | kind 78 | name 79 | ofType { 80 | kind 81 | name 82 | ofType { 83 | kind 84 | name 85 | ofType { 86 | kind 87 | name 88 | ofType { 89 | kind 90 | name 91 | ofType { 92 | kind 93 | name 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /graphql_client_cli/src/graphql/introspection_query_with_is_one_of.graphql: -------------------------------------------------------------------------------- 1 | query IntrospectionQueryWithIsOneOf { 2 | __schema { 3 | queryType { 4 | name 5 | } 6 | mutationType { 7 | name 8 | } 9 | subscriptionType { 10 | name 11 | } 12 | types { 13 | ...FullTypeWithisOneOf 14 | } 15 | directives { 16 | name 17 | description 18 | locations 19 | args { 20 | ...InputValue 21 | } 22 | } 23 | } 24 | } 25 | 26 | fragment FullTypeWithisOneOf on __Type { 27 | kind 28 | name 29 | description 30 | isOneOf 31 | fields(includeDeprecated: true) { 32 | name 33 | description 34 | args { 35 | ...InputValue 36 | } 37 | type { 38 | ...TypeRef 39 | } 40 | isDeprecated 41 | deprecationReason 42 | } 43 | inputFields { 44 | ...InputValue 45 | } 46 | interfaces { 47 | ...TypeRef 48 | } 49 | enumValues(includeDeprecated: true) { 50 | name 51 | description 52 | isDeprecated 53 | deprecationReason 54 | } 55 | possibleTypes { 56 | ...TypeRef 57 | } 58 | } 59 | 60 | fragment InputValue on __InputValue { 61 | name 62 | description 63 | type { 64 | ...TypeRef 65 | } 66 | defaultValue 67 | } 68 | 69 | fragment TypeRef on __Type { 70 | kind 71 | name 72 | ofType { 73 | kind 74 | name 75 | ofType { 76 | kind 77 | name 78 | ofType { 79 | kind 80 | name 81 | ofType { 82 | kind 83 | name 84 | ofType { 85 | kind 86 | name 87 | ofType { 88 | kind 89 | name 90 | ofType { 91 | kind 92 | name 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /graphql_client_cli/src/graphql/introspection_query_with_specified_by.graphql: -------------------------------------------------------------------------------- 1 | query IntrospectionQueryWithSpecifiedBy { 2 | __schema { 3 | queryType { 4 | name 5 | } 6 | mutationType { 7 | name 8 | } 9 | subscriptionType { 10 | name 11 | } 12 | types { 13 | ...FullTypeWithSpecifiedBy 14 | } 15 | directives { 16 | name 17 | description 18 | locations 19 | args { 20 | ...InputValue 21 | } 22 | } 23 | } 24 | } 25 | 26 | fragment FullTypeWithSpecifiedBy on __Type { 27 | kind 28 | name 29 | description 30 | specifiedByURL 31 | fields(includeDeprecated: true) { 32 | name 33 | description 34 | args { 35 | ...InputValue 36 | } 37 | type { 38 | ...TypeRef 39 | } 40 | isDeprecated 41 | deprecationReason 42 | } 43 | inputFields { 44 | ...InputValue 45 | } 46 | interfaces { 47 | ...TypeRef 48 | } 49 | enumValues(includeDeprecated: true) { 50 | name 51 | description 52 | isDeprecated 53 | deprecationReason 54 | } 55 | possibleTypes { 56 | ...TypeRef 57 | } 58 | } 59 | 60 | fragment InputValue on __InputValue { 61 | name 62 | description 63 | type { 64 | ...TypeRef 65 | } 66 | defaultValue 67 | } 68 | 69 | fragment TypeRef on __Type { 70 | kind 71 | name 72 | ofType { 73 | kind 74 | name 75 | ofType { 76 | kind 77 | name 78 | ofType { 79 | kind 80 | name 81 | ofType { 82 | kind 83 | name 84 | ofType { 85 | kind 86 | name 87 | ofType { 88 | kind 89 | name 90 | ofType { 91 | kind 92 | name 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /graphql_client_cli/src/graphql/introspection_schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | } 4 | 5 | type Query { 6 | __schema: __Schema 7 | } 8 | 9 | type __Schema { 10 | types: [__Type!]! 11 | queryType: __Type! 12 | mutationType: __Type 13 | subscriptionType: __Type 14 | directives: [__Directive!]! 15 | } 16 | 17 | type __Type { 18 | kind: __TypeKind! 19 | name: String 20 | description: String 21 | 22 | # OBJECT and INTERFACE only 23 | fields(includeDeprecated: Boolean = false): [__Field!] 24 | 25 | # OBJECT only 26 | interfaces: [__Type!] 27 | 28 | # INTERFACE and UNION only 29 | possibleTypes: [__Type!] 30 | 31 | # ENUM only 32 | enumValues(includeDeprecated: Boolean = false): [__EnumValue!] 33 | 34 | # INPUT_OBJECT only 35 | inputFields: [__InputValue!] 36 | 37 | # NON_NULL and LIST only 38 | ofType: __Type 39 | 40 | # may be non-null for custom SCALAR, otherwise null. 41 | # https://spec.graphql.org/draft/#sec-Scalars.Custom-Scalars 42 | specifiedByURL: String 43 | specifiedBy: String 44 | 45 | # should be non-null for INPUT_OBJECT only 46 | isOneOf: Boolean 47 | } 48 | 49 | type __Field { 50 | name: String! 51 | description: String 52 | args: [__InputValue!]! 53 | type: __Type! 54 | isDeprecated: Boolean! 55 | deprecationReason: String 56 | } 57 | 58 | type __InputValue { 59 | name: String! 60 | description: String 61 | type: __Type! 62 | defaultValue: String 63 | } 64 | 65 | type __EnumValue { 66 | name: String! 67 | description: String 68 | isDeprecated: Boolean! 69 | deprecationReason: String 70 | } 71 | 72 | enum __TypeKind { 73 | SCALAR 74 | OBJECT 75 | INTERFACE 76 | UNION 77 | ENUM 78 | INPUT_OBJECT 79 | LIST 80 | NON_NULL 81 | } 82 | 83 | type __Directive { 84 | name: String! 85 | description: String 86 | locations: [__DirectiveLocation!]! 87 | args: [__InputValue!]! 88 | } 89 | 90 | enum __DirectiveLocation { 91 | QUERY 92 | MUTATION 93 | SUBSCRIPTION 94 | FIELD 95 | FRAGMENT_DEFINITION 96 | FRAGMENT_SPREAD 97 | INLINE_FRAGMENT 98 | SCHEMA 99 | SCALAR 100 | OBJECT 101 | FIELD_DEFINITION 102 | ARGUMENT_DEFINITION 103 | INTERFACE 104 | UNION 105 | ENUM 106 | ENUM_VALUE 107 | INPUT_OBJECT 108 | INPUT_FIELD_DEFINITION 109 | } 110 | -------------------------------------------------------------------------------- /graphql_client_cli/src/introspection_queries.rs: -------------------------------------------------------------------------------- 1 | use graphql_client::GraphQLQuery; 2 | 3 | #[derive(GraphQLQuery)] 4 | #[graphql( 5 | schema_path = "src/graphql/introspection_schema.graphql", 6 | query_path = "src/graphql/introspection_query.graphql", 7 | response_derives = "Serialize", 8 | variable_derives = "Deserialize" 9 | )] 10 | #[allow(dead_code)] 11 | pub struct IntrospectionQuery; 12 | 13 | #[derive(GraphQLQuery)] 14 | #[graphql( 15 | schema_path = "src/graphql/introspection_schema.graphql", 16 | query_path = "src/graphql/introspection_query_with_is_one_of.graphql", 17 | response_derives = "Serialize", 18 | variable_derives = "Deserialize" 19 | )] 20 | #[allow(dead_code)] 21 | pub struct IntrospectionQueryWithIsOneOf; 22 | 23 | #[derive(GraphQLQuery)] 24 | #[graphql( 25 | schema_path = "src/graphql/introspection_schema.graphql", 26 | query_path = "src/graphql/introspection_query_with_specified_by.graphql", 27 | response_derives = "Serialize", 28 | variable_derives = "Deserialize" 29 | )] 30 | #[allow(dead_code)] 31 | pub struct IntrospectionQueryWithSpecifiedBy; 32 | 33 | #[derive(GraphQLQuery)] 34 | #[graphql( 35 | schema_path = "src/graphql/introspection_schema.graphql", 36 | query_path = "src/graphql/introspection_query_with_isOneOf_specifiedByUrl.graphql", 37 | response_derives = "Serialize", 38 | variable_derives = "Deserialize" 39 | )] 40 | #[allow(dead_code)] 41 | pub struct IntrospectionQueryWithIsOneOfSpecifiedByURL; 42 | -------------------------------------------------------------------------------- /graphql_client_cli/src/introspection_schema.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use crate::CliResult; 3 | use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE}; 4 | use std::path::PathBuf; 5 | use std::str::FromStr; 6 | 7 | use crate::introspection_queries::{ 8 | introspection_query, introspection_query_with_is_one_of, 9 | introspection_query_with_is_one_of_specified_by_url, introspection_query_with_specified_by, 10 | }; 11 | 12 | pub fn introspect_schema( 13 | location: &str, 14 | output: Option, 15 | authorization: Option, 16 | headers: Vec
, 17 | no_ssl: bool, 18 | is_one_of: bool, 19 | specify_by_url: bool, 20 | ) -> CliResult<()> { 21 | use std::io::Write; 22 | 23 | let out: Box = match output { 24 | Some(path) => Box::new(::std::fs::File::create(path)?), 25 | None => Box::new(std::io::stdout()), 26 | }; 27 | 28 | let mut request_body: graphql_client::QueryBody<()> = graphql_client::QueryBody { 29 | variables: (), 30 | query: introspection_query::QUERY, 31 | operation_name: introspection_query::OPERATION_NAME, 32 | }; 33 | 34 | if is_one_of { 35 | request_body = graphql_client::QueryBody { 36 | variables: (), 37 | query: introspection_query_with_is_one_of::QUERY, 38 | operation_name: introspection_query_with_is_one_of::OPERATION_NAME, 39 | } 40 | } 41 | 42 | if specify_by_url { 43 | request_body = graphql_client::QueryBody { 44 | variables: (), 45 | query: introspection_query_with_specified_by::QUERY, 46 | operation_name: introspection_query_with_specified_by::OPERATION_NAME, 47 | } 48 | } 49 | 50 | if is_one_of && specify_by_url { 51 | request_body = graphql_client::QueryBody { 52 | variables: (), 53 | query: introspection_query_with_is_one_of_specified_by_url::QUERY, 54 | operation_name: introspection_query_with_is_one_of_specified_by_url::OPERATION_NAME, 55 | } 56 | } 57 | 58 | let client = reqwest::blocking::Client::builder() 59 | .danger_accept_invalid_certs(no_ssl) 60 | .build()?; 61 | 62 | let mut req_builder = client.post(location).headers(construct_headers()); 63 | 64 | for custom_header in headers { 65 | req_builder = req_builder.header(custom_header.name.as_str(), custom_header.value.as_str()); 66 | } 67 | 68 | if let Some(token) = authorization { 69 | req_builder = req_builder.bearer_auth(token.as_str()); 70 | }; 71 | 72 | let res = req_builder.json(&request_body).send()?; 73 | 74 | if res.status().is_success() { 75 | // do nothing 76 | } else if res.status().is_server_error() { 77 | return Err(Error::message("server error!".into())); 78 | } else { 79 | let status = res.status(); 80 | let error_message = match res.text() { 81 | Ok(msg) => match serde_json::from_str::(&msg) { 82 | Ok(json) => format!("HTTP {}\n{}", status, serde_json::to_string_pretty(&json)?), 83 | Err(_) => format!("HTTP {}: {}", status, msg), 84 | }, 85 | Err(_) => format!("HTTP {}", status), 86 | }; 87 | return Err(Error::message(error_message)); 88 | } 89 | 90 | let json: serde_json::Value = res.json()?; 91 | serde_json::to_writer_pretty(out, &json)?; 92 | 93 | Ok(()) 94 | } 95 | 96 | fn construct_headers() -> HeaderMap { 97 | let mut headers = HeaderMap::new(); 98 | headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); 99 | headers.insert(ACCEPT, HeaderValue::from_static("application/json")); 100 | headers 101 | } 102 | 103 | #[derive(Debug, PartialEq, Eq, Clone)] 104 | pub struct Header { 105 | name: String, 106 | value: String, 107 | } 108 | 109 | impl FromStr for Header { 110 | type Err = String; 111 | 112 | fn from_str(input: &str) -> Result { 113 | // error: colon required for name/value pair 114 | if !input.contains(':') { 115 | return Err(format!( 116 | "Invalid header input. A colon is required to separate the name and value. [{}]", 117 | input 118 | )); 119 | } 120 | 121 | // split on first colon and trim whitespace from name and value 122 | let name_value: Vec<&str> = input.splitn(2, ':').collect(); 123 | let name = name_value[0].trim(); 124 | let value = name_value[1].trim(); 125 | 126 | // error: field name must be 127 | if name.is_empty() { 128 | return Err(format!( 129 | "Invalid header input. Field name is required before colon. [{}]", 130 | input 131 | )); 132 | } 133 | 134 | // error: no whitespace in field name 135 | if name.split_whitespace().count() > 1 { 136 | return Err(format!( 137 | "Invalid header input. Whitespace not allowed in field name. [{}]", 138 | input 139 | )); 140 | } 141 | 142 | Ok(Self { 143 | name: name.to_string(), 144 | value: value.to_string(), 145 | }) 146 | } 147 | } 148 | 149 | #[cfg(test)] 150 | mod tests { 151 | use super::*; 152 | 153 | #[test] 154 | fn it_errors_invalid_headers() { 155 | // https://tools.ietf.org/html/rfc7230#section-3.2 156 | 157 | for input in [ 158 | "X-Name Value", // error: colon required for name/value pair 159 | ": Value", // error: field name must be 160 | "X Name: Value", // error: no whitespace in field name 161 | "X\tName: Value", // error: no whitespace in field name (tab) 162 | ] 163 | .iter() 164 | { 165 | let header = Header::from_str(input); 166 | 167 | assert!(header.is_err(), "Expected error: [{}]", input); 168 | } 169 | } 170 | 171 | #[test] 172 | fn it_parses_valid_headers() { 173 | // https://tools.ietf.org/html/rfc7230#section-3.2 174 | 175 | let expected1 = Header { 176 | name: "X-Name".to_string(), 177 | value: "Value".to_string(), 178 | }; 179 | let expected2 = Header { 180 | name: "X-Name".to_string(), 181 | value: "Value:".to_string(), 182 | }; 183 | 184 | for (input, expected) in [ 185 | ("X-Name: Value", &expected1), // ideal 186 | ("X-Name:Value", &expected1), // no optional whitespace 187 | ("X-Name: Value ", &expected1), // with optional whitespace 188 | ("X-Name:\tValue", &expected1), // with optional whitespace (tab) 189 | ("X-Name: Value:", &expected2), // with colon in value 190 | // not allowed per RFC, but we'll forgive 191 | ("X-Name : Value", &expected1), 192 | (" X-Name: Value", &expected1), 193 | ] 194 | .iter() 195 | { 196 | let header = Header::from_str(input); 197 | 198 | assert!(header.is_ok(), "Expected ok: [{}]", input); 199 | assert_eq!( 200 | header.unwrap(), 201 | **expected, 202 | "Expected equality: [{}]", 203 | input 204 | ); 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /graphql_client_cli/src/main.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod generate; 3 | mod introspection_queries; 4 | mod introspection_schema; 5 | 6 | use clap::Parser; 7 | use env_logger::fmt::{Color, Style, StyledValue}; 8 | use log::Level; 9 | 10 | use error::Error; 11 | 12 | use std::path::PathBuf; 13 | use Cli::Generate; 14 | 15 | type CliResult = Result; 16 | 17 | #[derive(Parser)] 18 | #[clap(author, about, version)] 19 | enum Cli { 20 | /// Get the schema from a live GraphQL API. The schema is printed to stdout. 21 | #[clap(name = "introspect-schema")] 22 | IntrospectSchema { 23 | /// The URL of a GraphQL endpoint to introspect. 24 | schema_location: String, 25 | /// Where to write the JSON for the introspected schema. 26 | #[arg(long = "output")] 27 | output: Option, 28 | /// Set the contents of the Authorization header. 29 | #[arg(long = "authorization")] 30 | authorization: Option, 31 | /// Specify custom headers. 32 | /// --header 'X-Name: Value' 33 | #[arg(long = "header")] 34 | headers: Vec, 35 | /// Disable ssl verification. 36 | /// Default value is false. 37 | #[clap(long = "no-ssl")] 38 | no_ssl: bool, 39 | /// Introspection Option: is-one-of will enable the @oneOf directive in the introspection query. 40 | /// This is an proposed feature and is not compatible with many GraphQL servers. 41 | /// Default value is false. 42 | #[clap(long = "is-one-of")] 43 | is_one_of: bool, 44 | /// Introspection Option: specify-by-url will enable the @specifiedByURL directive in the introspection query. 45 | /// This is an proposed feature and is not compatible with many GraphQL servers. 46 | /// Default value is false. 47 | #[clap(long = "specify-by-url")] 48 | specify_by_url: bool, 49 | }, 50 | #[clap(name = "generate")] 51 | Generate { 52 | /// Path to GraphQL schema file (.json or .graphql). 53 | #[clap(short = 's', long = "schema-path")] 54 | schema_path: PathBuf, 55 | /// Path to the GraphQL query file. 56 | query_path: PathBuf, 57 | /// Name of target query. If you don't set this parameter, cli generate all queries in query file. 58 | #[clap(long = "selected-operation")] 59 | selected_operation: Option, 60 | /// Additional derives that will be added to the generated structs and enums for the variables. 61 | /// --variables-derives='Serialize,PartialEq' 62 | #[clap(short = 'I', long = "variables-derives")] 63 | variables_derives: Option, 64 | /// Additional derives that will be added to the generated structs and enums for the response. 65 | /// --response-derives='Serialize,PartialEq' 66 | #[clap(short = 'O', long = "response-derives")] 67 | response_derives: Option, 68 | /// You can choose deprecation strategy from allow, deny, or warn. 69 | /// Default value is warn. 70 | #[clap(short = 'd', long = "deprecation-strategy")] 71 | deprecation_strategy: Option, 72 | /// If you don't want to execute rustfmt to generated code, set this option. 73 | /// Default value is false. 74 | #[clap(long = "no-formatting")] 75 | no_formatting: bool, 76 | /// You can choose module and target struct visibility from pub and private. 77 | /// Default value is pub. 78 | #[clap(short = 'm', long = "module-visibility")] 79 | module_visibility: Option, 80 | /// The directory in which the code will be generated. 81 | /// 82 | /// If this option is omitted, the code will be generated next to the .graphql 83 | /// file, with the same name and the .rs extension. 84 | #[clap(short = 'o', long = "output-directory")] 85 | output_directory: Option, 86 | /// The module where the custom scalar definitions are located. 87 | /// --custom-scalars-module='crate::gql::custom_scalars' 88 | #[clap(short = 'p', long = "custom-scalars-module")] 89 | custom_scalars_module: Option, 90 | /// A flag indicating if the enum representing the variants of a fragment union/interface should have a "other" variant 91 | /// --fragments-other-variant 92 | #[clap(long = "fragments-other-variant")] 93 | fragments_other_variant: bool, 94 | /// List of externally defined enum types. Type names must match those used in the schema exactly 95 | #[clap(long = "external-enums", num_args(0..), action(clap::ArgAction::Append))] 96 | external_enums: Option>, 97 | /// Custom variable types to use 98 | /// --custom-variable-types='external_crate::MyStruct,external_crate::MyStruct2' 99 | #[clap(long = "custom-variable_types")] 100 | custom_variable_types: Option, 101 | /// Custom response type to use 102 | /// --custom-response-type='external_crate::MyResponse' 103 | #[clap(long = "custom-response-type")] 104 | custom_response_type: Option, 105 | }, 106 | } 107 | 108 | fn main() -> CliResult<()> { 109 | set_env_logger(); 110 | 111 | let cli = Cli::parse(); 112 | match cli { 113 | Cli::IntrospectSchema { 114 | schema_location, 115 | output, 116 | authorization, 117 | headers, 118 | no_ssl, 119 | is_one_of, 120 | specify_by_url, 121 | } => introspection_schema::introspect_schema( 122 | &schema_location, 123 | output, 124 | authorization, 125 | headers, 126 | no_ssl, 127 | is_one_of, 128 | specify_by_url, 129 | ), 130 | Generate { 131 | variables_derives, 132 | response_derives, 133 | deprecation_strategy, 134 | module_visibility, 135 | no_formatting, 136 | output_directory, 137 | query_path, 138 | schema_path, 139 | selected_operation, 140 | custom_scalars_module, 141 | fragments_other_variant, 142 | external_enums, 143 | custom_variable_types, 144 | custom_response_type, 145 | } => generate::generate_code(generate::CliCodegenParams { 146 | query_path, 147 | schema_path, 148 | selected_operation, 149 | variables_derives, 150 | response_derives, 151 | deprecation_strategy, 152 | no_formatting, 153 | module_visibility, 154 | output_directory, 155 | custom_scalars_module, 156 | fragments_other_variant, 157 | external_enums, 158 | custom_variable_types, 159 | custom_response_type, 160 | }), 161 | } 162 | } 163 | 164 | fn set_env_logger() { 165 | use std::io::Write; 166 | 167 | env_logger::Builder::from_default_env() 168 | .format(|f, record| { 169 | let mut style = f.style(); 170 | let level = colored_level(&mut style, record.level()); 171 | let mut style = f.style(); 172 | let file = style.set_bold(true).value("file"); 173 | let mut style = f.style(); 174 | let module = style.set_bold(true).value("module"); 175 | writeln!( 176 | f, 177 | "{} {}: {} {}: {}\n{}", 178 | level, 179 | file, 180 | record.file().unwrap(), 181 | module, 182 | record.target(), 183 | record.args() 184 | ) 185 | }) 186 | .init(); 187 | } 188 | 189 | fn colored_level(style: &mut Style, level: Level) -> StyledValue<'_, &'static str> { 190 | match level { 191 | Level::Trace => style.set_color(Color::Magenta).value("TRACE"), 192 | Level::Debug => style.set_color(Color::Blue).value("DEBUG"), 193 | Level::Info => style.set_color(Color::Green).value("INFO "), 194 | Level::Warn => style.set_color(Color::Yellow).value("WARN "), 195 | Level::Error => style.set_color(Color::Red).value("ERROR"), 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /graphql_client_codegen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphql_client_codegen" 3 | version = "0.14.0" 4 | authors = ["Tom Houlé "] 5 | description = "Utility crate for graphql_client" 6 | license = "Apache-2.0 OR MIT" 7 | repository = "https://github.com/graphql-rust/graphql-client" 8 | edition = "2018" 9 | 10 | [dependencies] 11 | graphql-introspection-query = { version = "0.2.0", path = "../graphql-introspection-query" } 12 | graphql-parser = "0.4" 13 | heck = ">=0.4, <=0.5" 14 | lazy_static = "1.3" 15 | proc-macro2 = { version = "^1.0", features = [] } 16 | quote = "^1.0" 17 | serde_json = "1.0" 18 | serde = { version = "^1.0", features = ["derive"] } 19 | syn = { version = "^2.0", features = [ "full" ] } 20 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/codegen/enums.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | codegen::render_derives, codegen_options::GraphQLClientCodegenOptions, query::BoundQuery, 3 | }; 4 | use proc_macro2::{Ident, Span, TokenStream}; 5 | use quote::quote; 6 | 7 | /** 8 | * About rust keyword escaping: variant_names and constructors must be escaped, 9 | * variant_str not. 10 | * Example schema: enum AnEnum { where \n self } 11 | * Generated "variant_names" enum: pub enum AnEnum { where_, self_, Other(String), } 12 | * Generated serialize line: "AnEnum::where_ => "where"," 13 | */ 14 | pub(super) fn generate_enum_definitions<'a, 'schema: 'a>( 15 | all_used_types: &'a crate::query::UsedTypes, 16 | options: &'a GraphQLClientCodegenOptions, 17 | query: BoundQuery<'schema>, 18 | ) -> impl Iterator + 'a { 19 | let serde = options.serde_path(); 20 | let traits = options 21 | .all_response_derives() 22 | .chain(options.all_variable_derives()) 23 | .filter(|d| !&["Serialize", "Deserialize", "Default"].contains(d)) 24 | // Use BTreeSet instead of HashSet for a stable ordering. 25 | .collect::>(); 26 | let derives = render_derives(traits.into_iter()); 27 | let normalization = options.normalization(); 28 | 29 | all_used_types.enums(query.schema) 30 | .filter(move |(_id, r#enum)| !options.extern_enums().contains(&r#enum.name)) 31 | .map(move |(_id, r#enum)| { 32 | let variant_names: Vec = r#enum 33 | .variants 34 | .iter() 35 | .map(|v| { 36 | let safe_name = super::shared::keyword_replace(v.as_str()); 37 | let name = normalization.enum_variant(safe_name.as_ref()); 38 | let name = Ident::new(&name, Span::call_site()); 39 | 40 | quote!(#name) 41 | }) 42 | .collect(); 43 | let variant_names = &variant_names; 44 | let name_ident = normalization.enum_name(r#enum.name.as_str()); 45 | let name_ident = Ident::new(&name_ident, Span::call_site()); 46 | let constructors: Vec<_> = r#enum 47 | .variants 48 | .iter() 49 | .map(|v| { 50 | let safe_name = super::shared::keyword_replace(v); 51 | let name = normalization.enum_variant(safe_name.as_ref()); 52 | let v = Ident::new(&name, Span::call_site()); 53 | 54 | quote!(#name_ident::#v) 55 | }) 56 | .collect(); 57 | let constructors = &constructors; 58 | let variant_str: Vec<&str> = r#enum.variants.iter().map(|s| s.as_str()).collect(); 59 | let variant_str = &variant_str; 60 | 61 | let name = name_ident; 62 | 63 | quote! { 64 | #derives 65 | pub enum #name { 66 | #(#variant_names,)* 67 | Other(String), 68 | } 69 | 70 | impl #serde::Serialize for #name { 71 | fn serialize(&self, ser: S) -> Result { 72 | ser.serialize_str(match *self { 73 | #(#constructors => #variant_str,)* 74 | #name::Other(ref s) => &s, 75 | }) 76 | } 77 | } 78 | 79 | impl<'de> #serde::Deserialize<'de> for #name { 80 | fn deserialize>(deserializer: D) -> Result { 81 | let s: String = #serde::Deserialize::deserialize(deserializer)?; 82 | 83 | match s.as_str() { 84 | #(#variant_str => Ok(#constructors),)* 85 | _ => Ok(#name::Other(s)), 86 | } 87 | } 88 | } 89 | }}) 90 | } 91 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/codegen/inputs.rs: -------------------------------------------------------------------------------- 1 | use super::shared::{field_rename_annotation, keyword_replace}; 2 | use crate::{ 3 | codegen_options::GraphQLClientCodegenOptions, 4 | query::{BoundQuery, UsedTypes}, 5 | schema::{input_is_recursive_without_indirection, StoredInputType}, 6 | type_qualifiers::GraphqlTypeQualifier, 7 | }; 8 | use heck::{ToSnakeCase, ToUpperCamelCase}; 9 | use proc_macro2::{Ident, Span, TokenStream}; 10 | use quote::{quote, ToTokens}; 11 | 12 | pub(super) fn generate_input_object_definitions( 13 | all_used_types: &UsedTypes, 14 | options: &GraphQLClientCodegenOptions, 15 | variable_derives: &impl quote::ToTokens, 16 | query: &BoundQuery<'_>, 17 | ) -> Vec { 18 | let custom_variable_types = options.custom_variable_types(); 19 | all_used_types 20 | .inputs(query.schema) 21 | .map(|(input_id, input)| { 22 | let custom_variable_type = query.query.variables.iter() 23 | .enumerate() 24 | .find(|(_, v) | v.r#type.id.as_input_id().is_some_and(|i| i == input_id)) 25 | .map(|(index, _)| custom_variable_types.get(index)) 26 | .flatten(); 27 | if let Some(custom_type) = custom_variable_type { 28 | generate_type_def(input, options, custom_type) 29 | } else if input.is_one_of { 30 | generate_enum(input, options, variable_derives, query) 31 | } else { 32 | generate_struct(input, options, variable_derives, query) 33 | } 34 | }) 35 | .collect() 36 | } 37 | 38 | fn generate_type_def( 39 | input: &StoredInputType, 40 | options: &GraphQLClientCodegenOptions, 41 | custom_type: &String, 42 | ) -> TokenStream { 43 | let custom_type = syn::parse_str::(custom_type).unwrap(); 44 | let normalized_name = options.normalization().input_name(input.name.as_str()); 45 | let safe_name = keyword_replace(normalized_name); 46 | let struct_name = Ident::new(safe_name.as_ref(), Span::call_site()); 47 | quote!(pub type #struct_name = #custom_type;) 48 | } 49 | 50 | fn generate_struct( 51 | input: &StoredInputType, 52 | options: &GraphQLClientCodegenOptions, 53 | variable_derives: &impl quote::ToTokens, 54 | query: &BoundQuery<'_>, 55 | ) -> TokenStream { 56 | let serde = options.serde_path(); 57 | let serde_path = serde.to_token_stream().to_string(); 58 | 59 | let normalized_name = options.normalization().input_name(input.name.as_str()); 60 | let safe_name = keyword_replace(normalized_name); 61 | let struct_name = Ident::new(safe_name.as_ref(), Span::call_site()); 62 | 63 | let fields = input.fields.iter().map(|(field_name, field_type)| { 64 | let safe_field_name = keyword_replace(field_name.to_snake_case()); 65 | let annotation = field_rename_annotation(field_name, safe_field_name.as_ref()); 66 | let name_ident = Ident::new(safe_field_name.as_ref(), Span::call_site()); 67 | let normalized_field_type_name = options 68 | .normalization() 69 | .field_type(field_type.id.name(query.schema)); 70 | let optional_skip_serializing_none = 71 | if *options.skip_serializing_none() && field_type.is_optional() { 72 | Some(quote!(#[serde(skip_serializing_if = "Option::is_none")])) 73 | } else { 74 | None 75 | }; 76 | let type_name = Ident::new(normalized_field_type_name.as_ref(), Span::call_site()); 77 | let field_type_tokens = super::decorate_type(&type_name, &field_type.qualifiers); 78 | let field_type = if field_type 79 | .id 80 | .as_input_id() 81 | .map(|input_id| input_is_recursive_without_indirection(input_id, query.schema)) 82 | .unwrap_or(false) 83 | { 84 | quote!(Box<#field_type_tokens>) 85 | } else { 86 | field_type_tokens 87 | }; 88 | 89 | quote!( 90 | #optional_skip_serializing_none 91 | #annotation pub #name_ident: #field_type 92 | ) 93 | }); 94 | 95 | quote! { 96 | #variable_derives 97 | #[serde(crate = #serde_path)] 98 | pub struct #struct_name{ 99 | #(#fields,)* 100 | } 101 | } 102 | } 103 | 104 | fn generate_enum( 105 | input: &StoredInputType, 106 | options: &GraphQLClientCodegenOptions, 107 | variable_derives: &impl quote::ToTokens, 108 | query: &BoundQuery<'_>, 109 | ) -> TokenStream { 110 | let normalized_name = options.normalization().input_name(input.name.as_str()); 111 | let safe_name = keyword_replace(normalized_name); 112 | let enum_name = Ident::new(safe_name.as_ref(), Span::call_site()); 113 | 114 | let variants = input.fields.iter().map(|(field_name, field_type)| { 115 | let variant_name = field_name.to_upper_camel_case(); 116 | let safe_variant_name = keyword_replace(&variant_name); 117 | 118 | let annotation = field_rename_annotation(field_name.as_ref(), &variant_name); 119 | let name_ident = Ident::new(safe_variant_name.as_ref(), Span::call_site()); 120 | 121 | let normalized_field_type_name = options 122 | .normalization() 123 | .field_type(field_type.id.name(query.schema)); 124 | let type_name = Ident::new(normalized_field_type_name.as_ref(), Span::call_site()); 125 | 126 | // Add the required qualifier so that the variant's field isn't wrapped in Option 127 | let mut qualifiers = vec![GraphqlTypeQualifier::Required]; 128 | qualifiers.extend(field_type.qualifiers.iter().cloned()); 129 | 130 | let field_type_tokens = super::decorate_type(&type_name, &qualifiers); 131 | let field_type = if field_type 132 | .id 133 | .as_input_id() 134 | .map(|input_id| input_is_recursive_without_indirection(input_id, query.schema)) 135 | .unwrap_or(false) 136 | { 137 | quote!(Box<#field_type_tokens>) 138 | } else { 139 | field_type_tokens 140 | }; 141 | 142 | quote!( 143 | #annotation #name_ident(#field_type) 144 | ) 145 | }); 146 | 147 | quote! { 148 | #variable_derives 149 | pub enum #enum_name{ 150 | #(#variants,)* 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/codegen/shared.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use std::borrow::Cow; 4 | 5 | // List of keywords based on https://doc.rust-lang.org/reference/keywords.html 6 | // code snippet: `[...new Set($$("code.hljs").map(x => x.textContent).filter(x => x.match(/^[_a-z0-9]+$/i)))].sort()` 7 | const RUST_KEYWORDS: &[&str] = &[ 8 | "Self", "abstract", "as", "async", "await", "become", "box", "break", "const", "continue", 9 | "crate", "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "if", "impl", 10 | "in", "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub", "ref", 11 | "return", "self", "static", "struct", "super", "trait", "true", "try", "type", "typeof", 12 | "union", "unsafe", "unsized", "use", "virtual", "where", "while", "yield", 13 | ]; 14 | 15 | pub(crate) fn keyword_replace<'a>(needle: impl Into>) -> Cow<'a, str> { 16 | let needle = needle.into(); 17 | match RUST_KEYWORDS.binary_search(&needle.as_ref()) { 18 | Ok(index) => [RUST_KEYWORDS[index], "_"].concat().into(), 19 | Err(_) => needle, 20 | } 21 | } 22 | 23 | /// Given the GraphQL schema name for an object/interface/input object field and 24 | /// the equivalent rust name, produces a serde annotation to map them during 25 | /// (de)serialization if it is necessary, otherwise an empty TokenStream. 26 | pub(crate) fn field_rename_annotation(graphql_name: &str, rust_name: &str) -> Option { 27 | if graphql_name != rust_name { 28 | Some(quote!(#[serde(rename = #graphql_name)])) 29 | } else { 30 | None 31 | } 32 | } 33 | 34 | #[cfg(test)] 35 | mod tests { 36 | #[test] 37 | fn keyword_replace_works() { 38 | use super::keyword_replace; 39 | assert_eq!("fora", keyword_replace("fora")); 40 | assert_eq!("in_", keyword_replace("in")); 41 | assert_eq!("fn_", keyword_replace("fn")); 42 | assert_eq!("struct_", keyword_replace("struct")); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/constants.rs: -------------------------------------------------------------------------------- 1 | pub(crate) const TYPENAME_FIELD: &str = "__typename"; 2 | 3 | pub(crate) const MULTIPLE_SUBSCRIPTION_FIELDS_ERROR: &str = r##" 4 | Multiple-field queries on the root subscription field are forbidden by the spec. 5 | 6 | See: https://github.com/facebook/graphql/blob/master/spec/Section%205%20--%20Validation.md#subscription-operation-definitions 7 | "##; 8 | 9 | /// Error message when a selection set is the root of a query. 10 | pub(crate) const SELECTION_SET_AT_ROOT: &str = r#" 11 | Operations in queries must be named. 12 | 13 | Instead of this: 14 | 15 | { 16 | user { 17 | name 18 | repositories { 19 | name 20 | commits 21 | } 22 | } 23 | } 24 | 25 | Write this: 26 | 27 | query UserRepositories { 28 | user { 29 | name 30 | repositories { 31 | name 32 | commits 33 | } 34 | } 35 | } 36 | "#; 37 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/deprecation.rs: -------------------------------------------------------------------------------- 1 | /// Whether an item is deprecated, with context. 2 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] 3 | pub enum DeprecationStatus { 4 | /// Not deprecated 5 | Current, 6 | /// Deprecated 7 | Deprecated(Option), 8 | } 9 | 10 | /// The available deprecation strategies. 11 | #[derive(Debug, PartialEq, Eq, Clone, Default)] 12 | pub enum DeprecationStrategy { 13 | /// Allow use of deprecated items in queries, and say nothing. 14 | Allow, 15 | /// Fail compilation if a deprecated item is used. 16 | Deny, 17 | /// Allow use of deprecated items in queries, but warn about them (default). 18 | #[default] 19 | Warn, 20 | } 21 | 22 | impl std::str::FromStr for DeprecationStrategy { 23 | type Err = (); 24 | 25 | fn from_str(s: &str) -> Result { 26 | match s.trim() { 27 | "allow" => Ok(DeprecationStrategy::Allow), 28 | "deny" => Ok(DeprecationStrategy::Deny), 29 | "warn" => Ok(DeprecationStrategy::Warn), 30 | _ => Err(()), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/generated_module.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | codegen_options::*, 3 | query::{BoundQuery, OperationId}, 4 | BoxError, 5 | }; 6 | use heck::*; 7 | use proc_macro2::{Ident, Span, TokenStream}; 8 | use quote::quote; 9 | use std::{error::Error, fmt::Display}; 10 | 11 | #[derive(Debug)] 12 | struct OperationNotFound { 13 | operation_name: String, 14 | } 15 | 16 | impl Display for OperationNotFound { 17 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 18 | f.write_str("Could not find an operation named ")?; 19 | f.write_str(&self.operation_name)?; 20 | f.write_str(" in the query document.") 21 | } 22 | } 23 | 24 | impl Error for OperationNotFound {} 25 | 26 | /// This struct contains the parameters necessary to generate code for a given operation. 27 | pub(crate) struct GeneratedModule<'a> { 28 | pub operation: &'a str, 29 | pub query_string: &'a str, 30 | pub resolved_query: &'a crate::query::Query, 31 | pub schema: &'a crate::schema::Schema, 32 | pub options: &'a crate::GraphQLClientCodegenOptions, 33 | } 34 | 35 | impl GeneratedModule<'_> { 36 | /// Generate the items for the variables and the response that will go inside the module. 37 | fn build_impls(&self) -> Result { 38 | Ok(crate::codegen::response_for_query( 39 | self.root()?, 40 | self.options, 41 | BoundQuery { 42 | query: self.resolved_query, 43 | schema: self.schema, 44 | }, 45 | )?) 46 | } 47 | 48 | fn root(&self) -> Result { 49 | let op_name = self.options.normalization().operation(self.operation); 50 | self.resolved_query 51 | .select_operation(&op_name, *self.options.normalization()) 52 | .map(|op| op.0) 53 | .ok_or_else(|| OperationNotFound { 54 | operation_name: op_name.into(), 55 | }) 56 | } 57 | 58 | /// Generate the module and all the code inside. 59 | pub(crate) fn to_token_stream(&self) -> Result { 60 | let module_name = Ident::new(&self.operation.to_snake_case(), Span::call_site()); 61 | let module_visibility = &self.options.module_visibility(); 62 | let operation_name = self.operation; 63 | let operation_name_ident = self.options.normalization().operation(self.operation); 64 | let operation_name_ident = Ident::new(&operation_name_ident, Span::call_site()); 65 | 66 | // Force cargo to refresh the generated code when the query file changes. 67 | let query_include = self 68 | .options 69 | .query_file() 70 | .map(|path| { 71 | let path = path.to_str(); 72 | quote!( 73 | const __QUERY_WORKAROUND: &str = include_str!(#path); 74 | ) 75 | }) 76 | .unwrap_or_default(); 77 | 78 | let query_string = &self.query_string; 79 | let impls = self.build_impls()?; 80 | 81 | let struct_declaration: Option<_> = match self.options.mode { 82 | CodegenMode::Cli => Some(quote!(#module_visibility struct #operation_name_ident;)), 83 | // The struct is already present in derive mode. 84 | CodegenMode::Derive => None, 85 | }; 86 | 87 | Ok(quote!( 88 | #struct_declaration 89 | 90 | #module_visibility mod #module_name { 91 | #![allow(dead_code)] 92 | 93 | use std::result::Result; 94 | 95 | pub const OPERATION_NAME: &str = #operation_name; 96 | pub const QUERY: &str = #query_string; 97 | 98 | #query_include 99 | 100 | #impls 101 | } 102 | 103 | impl graphql_client::GraphQLQuery for #operation_name_ident { 104 | type Variables = #module_name::Variables; 105 | type ResponseData = #module_name::ResponseData; 106 | 107 | fn build_query(variables: Self::Variables) -> graphql_client::QueryBody { 108 | graphql_client::QueryBody { 109 | variables, 110 | query: #module_name::QUERY, 111 | operation_name: #module_name::OPERATION_NAME, 112 | } 113 | 114 | } 115 | } 116 | )) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![warn(rust_2018_idioms)] 3 | #![allow(clippy::option_option)] 4 | 5 | //! Crate for Rust code generation from a GraphQL query, schema, and options. 6 | 7 | use lazy_static::*; 8 | use proc_macro2::TokenStream; 9 | use quote::*; 10 | use schema::Schema; 11 | 12 | mod codegen; 13 | mod codegen_options; 14 | /// Deprecation-related code 15 | pub mod deprecation; 16 | /// Contains the [Schema] type and its implementation. 17 | pub mod schema; 18 | 19 | mod constants; 20 | mod generated_module; 21 | /// Normalization-related code 22 | pub mod normalization; 23 | mod query; 24 | mod type_qualifiers; 25 | 26 | #[cfg(test)] 27 | mod tests; 28 | 29 | pub use crate::codegen_options::{CodegenMode, GraphQLClientCodegenOptions}; 30 | 31 | use std::{collections::BTreeMap, fmt::Display, io}; 32 | 33 | #[derive(Debug)] 34 | struct GeneralError(String); 35 | 36 | impl Display for GeneralError { 37 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 38 | f.write_str(&self.0) 39 | } 40 | } 41 | 42 | impl std::error::Error for GeneralError {} 43 | 44 | type BoxError = Box; 45 | type CacheMap = std::sync::Mutex>; 46 | type QueryDocument = graphql_parser::query::Document<'static, String>; 47 | 48 | lazy_static! { 49 | static ref SCHEMA_CACHE: CacheMap = CacheMap::default(); 50 | static ref QUERY_CACHE: CacheMap<(String, QueryDocument)> = CacheMap::default(); 51 | } 52 | 53 | fn get_set_cached( 54 | cache: &CacheMap, 55 | key: &std::path::Path, 56 | value_func: impl FnOnce() -> T, 57 | ) -> T { 58 | let mut lock = cache.lock().expect("cache is poisoned"); 59 | lock.entry(key.into()).or_insert_with(value_func).clone() 60 | } 61 | 62 | fn query_document(query_string: &str) -> Result { 63 | let document = graphql_parser::parse_query(query_string) 64 | .map_err(|err| GeneralError(format!("Query parser error: {}", err)))? 65 | .into_static(); 66 | Ok(document) 67 | } 68 | 69 | fn get_set_query_from_file(query_path: &std::path::Path) -> (String, QueryDocument) { 70 | get_set_cached(&QUERY_CACHE, query_path, || { 71 | let query_string = read_file(query_path).unwrap(); 72 | let query_document = query_document(&query_string).unwrap(); 73 | (query_string, query_document) 74 | }) 75 | } 76 | 77 | fn get_set_schema_from_file(schema_path: &std::path::Path) -> Schema { 78 | get_set_cached(&SCHEMA_CACHE, schema_path, || { 79 | let schema_extension = schema_path 80 | .extension() 81 | .map(|ext| ext.to_str().expect("Path must be valid UTF-8")) 82 | .unwrap_or(""); 83 | let schema_string = read_file(schema_path).unwrap(); 84 | match schema_extension { 85 | "graphql" | "graphqls"| "gql" => { 86 | let s = graphql_parser::schema::parse_schema::<&str>(&schema_string).map_err(|parser_error| GeneralError(format!("Parser error: {}", parser_error))).unwrap(); 87 | Schema::from(s) 88 | } 89 | "json" => { 90 | let parsed: graphql_introspection_query::introspection_response::IntrospectionResponse = serde_json::from_str(&schema_string).unwrap(); 91 | Schema::from(parsed) 92 | } 93 | extension => panic!("Unsupported extension for the GraphQL schema: {} (only .json, .graphql, .graphqls and .gql are supported)", extension) 94 | } 95 | }) 96 | } 97 | 98 | /// Generates Rust code given a path to a query file, a path to a schema file, and options. 99 | pub fn generate_module_token_stream( 100 | query_path: std::path::PathBuf, 101 | schema_path: &std::path::Path, 102 | options: GraphQLClientCodegenOptions, 103 | ) -> Result { 104 | let query = get_set_query_from_file(query_path.as_path()); 105 | let schema = get_set_schema_from_file(schema_path); 106 | 107 | generate_module_token_stream_inner(&query, &schema, options) 108 | } 109 | 110 | /// Generates Rust code given a query string, a path to a schema file, and options. 111 | pub fn generate_module_token_stream_from_string( 112 | query_string: &str, 113 | schema_path: &std::path::Path, 114 | options: GraphQLClientCodegenOptions, 115 | ) -> Result { 116 | let query = (query_string.to_string(), query_document(query_string)?); 117 | let schema = get_set_schema_from_file(schema_path); 118 | 119 | generate_module_token_stream_inner(&query, &schema, options) 120 | } 121 | 122 | /// Generates Rust code given a query string and query document, a schema, and options. 123 | fn generate_module_token_stream_inner( 124 | query: &(String, QueryDocument), 125 | schema: &Schema, 126 | options: GraphQLClientCodegenOptions, 127 | ) -> Result { 128 | let (query_string, query_document) = query; 129 | 130 | // We need to qualify the query with the path to the crate it is part of 131 | let query = crate::query::resolve(schema, query_document)?; 132 | 133 | // Determine which operation we are generating code for. This will be used in operationName. 134 | let operations = options 135 | .operation_name 136 | .as_ref() 137 | .and_then(|operation_name| query.select_operation(operation_name, *options.normalization())) 138 | .map(|op| vec![op]); 139 | 140 | let operations = match (operations, &options.mode) { 141 | (Some(ops), _) => ops, 142 | (None, &CodegenMode::Cli) => query.operations().collect(), 143 | (None, &CodegenMode::Derive) => { 144 | return Err(GeneralError(derive_operation_not_found_error( 145 | options.struct_ident(), 146 | &query, 147 | )) 148 | .into()); 149 | } 150 | }; 151 | 152 | // The generated modules. 153 | let mut modules = Vec::with_capacity(operations.len()); 154 | 155 | for operation in &operations { 156 | let generated = generated_module::GeneratedModule { 157 | query_string: query_string.as_str(), 158 | schema, 159 | resolved_query: &query, 160 | operation: &operation.1.name, 161 | options: &options, 162 | } 163 | .to_token_stream()?; 164 | modules.push(generated); 165 | } 166 | 167 | let modules = quote! { #(#modules)* }; 168 | 169 | Ok(modules) 170 | } 171 | 172 | #[derive(Debug)] 173 | enum ReadFileError { 174 | FileNotFound { path: String, io_error: io::Error }, 175 | ReadError { path: String, io_error: io::Error }, 176 | } 177 | 178 | impl Display for ReadFileError { 179 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 180 | match self { 181 | ReadFileError::FileNotFound { path, .. } => { 182 | write!(f, "Could not find file with path: {}\n 183 | 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) 184 | } 185 | ReadFileError::ReadError { path, .. } => { 186 | f.write_str("Error reading file at: ")?; 187 | f.write_str(path) 188 | } 189 | } 190 | } 191 | } 192 | 193 | impl std::error::Error for ReadFileError { 194 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 195 | match self { 196 | ReadFileError::FileNotFound { io_error, .. } 197 | | ReadFileError::ReadError { io_error, .. } => Some(io_error), 198 | } 199 | } 200 | } 201 | 202 | fn read_file(path: &std::path::Path) -> Result { 203 | use std::fs; 204 | use std::io::prelude::*; 205 | 206 | let mut out = String::new(); 207 | let mut file = fs::File::open(path).map_err(|io_error| ReadFileError::FileNotFound { 208 | io_error, 209 | path: path.display().to_string(), 210 | })?; 211 | 212 | file.read_to_string(&mut out) 213 | .map_err(|io_error| ReadFileError::ReadError { 214 | io_error, 215 | path: path.display().to_string(), 216 | })?; 217 | Ok(out) 218 | } 219 | 220 | /// In derive mode, build an error when the operation with the same name as the struct is not found. 221 | fn derive_operation_not_found_error( 222 | ident: Option<&proc_macro2::Ident>, 223 | query: &crate::query::Query, 224 | ) -> String { 225 | let operation_name = ident.map(ToString::to_string); 226 | let struct_ident = operation_name.as_deref().unwrap_or(""); 227 | 228 | let available_operations: Vec<&str> = query 229 | .operations() 230 | .map(|(_id, op)| op.name.as_str()) 231 | .collect(); 232 | let available_operations: String = available_operations.join(", "); 233 | 234 | format!( 235 | "The struct name does not match any defined operation in the query file.\nStruct name: {}\nDefined operations: {}", 236 | struct_ident, 237 | available_operations, 238 | ) 239 | } 240 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/normalization.rs: -------------------------------------------------------------------------------- 1 | use heck::ToUpperCamelCase; 2 | use std::borrow::Cow; 3 | 4 | /// Normalization conventions available for generated code. 5 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 6 | pub enum Normalization { 7 | /// Use naming conventions from the schema. 8 | None, 9 | /// Use Rust naming conventions for generated code. 10 | Rust, 11 | } 12 | 13 | impl Normalization { 14 | fn camel_case(self, name: &str) -> Cow<'_, str> { 15 | match self { 16 | Self::None => name.into(), 17 | Self::Rust => name.to_upper_camel_case().into(), 18 | } 19 | } 20 | 21 | pub(crate) fn operation(self, op: &str) -> Cow<'_, str> { 22 | self.camel_case(op) 23 | } 24 | 25 | pub(crate) fn enum_variant(self, enm: &str) -> Cow<'_, str> { 26 | self.camel_case(enm) 27 | } 28 | 29 | pub(crate) fn enum_name(self, enm: &str) -> Cow<'_, str> { 30 | self.camel_case(enm) 31 | } 32 | 33 | fn field_type_impl(self, fty: &str) -> Cow<'_, str> { 34 | if fty == "ID" || fty.starts_with("__") { 35 | fty.into() 36 | } else { 37 | self.camel_case(fty) 38 | } 39 | } 40 | 41 | pub(crate) fn field_type(self, fty: &str) -> Cow<'_, str> { 42 | self.field_type_impl(fty) 43 | } 44 | 45 | pub(crate) fn input_name(self, inm: &str) -> Cow<'_, str> { 46 | self.camel_case(inm) 47 | } 48 | 49 | pub(crate) fn scalar_name(self, snm: &str) -> Cow<'_, str> { 50 | self.camel_case(snm) 51 | } 52 | } 53 | 54 | impl std::str::FromStr for Normalization { 55 | type Err = (); 56 | 57 | fn from_str(s: &str) -> Result { 58 | match s.trim() { 59 | "none" => Ok(Normalization::None), 60 | "rust" => Ok(Normalization::Rust), 61 | _ => Err(()), 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/query/fragments.rs: -------------------------------------------------------------------------------- 1 | use super::{Query, ResolvedFragmentId, SelectionId}; 2 | use crate::schema::TypeId; 3 | use heck::*; 4 | 5 | #[derive(Debug)] 6 | pub(crate) struct ResolvedFragment { 7 | pub(crate) name: String, 8 | pub(crate) on: TypeId, 9 | pub(crate) selection_set: Vec, 10 | } 11 | 12 | impl ResolvedFragment { 13 | pub(super) fn to_path_segment(&self) -> String { 14 | self.name.to_upper_camel_case() 15 | } 16 | } 17 | 18 | pub(crate) fn fragment_is_recursive(fragment_id: ResolvedFragmentId, query: &Query) -> bool { 19 | let fragment = query.get_fragment(fragment_id); 20 | 21 | query 22 | .walk_selection_set(&fragment.selection_set) 23 | .any(|(_id, selection)| selection.contains_fragment(fragment_id, query)) 24 | } 25 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/query/operations.rs: -------------------------------------------------------------------------------- 1 | use super::SelectionId; 2 | use crate::schema::ObjectId; 3 | use heck::*; 4 | 5 | #[derive(Debug, Clone)] 6 | pub(crate) enum OperationType { 7 | Query, 8 | Mutation, 9 | Subscription, 10 | } 11 | 12 | pub(crate) struct ResolvedOperation { 13 | pub(crate) name: String, 14 | pub(crate) _operation_type: OperationType, 15 | pub(crate) selection_set: Vec, 16 | pub(crate) object_id: ObjectId, 17 | } 18 | 19 | impl ResolvedOperation { 20 | pub(crate) fn to_path_segment(&self) -> String { 21 | self.name.to_upper_camel_case() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/query/validation.rs: -------------------------------------------------------------------------------- 1 | use super::{full_path_prefix, BoundQuery, Query, QueryValidationError, Selection, SelectionId}; 2 | use crate::schema::TypeId; 3 | 4 | pub(super) fn validate_typename_presence( 5 | query: &BoundQuery<'_>, 6 | ) -> Result<(), QueryValidationError> { 7 | for fragment in query.query.fragments.iter() { 8 | let type_id = match fragment.on { 9 | id @ TypeId::Interface(_) | id @ TypeId::Union(_) => id, 10 | _ => continue, 11 | }; 12 | 13 | if !selection_set_contains_type_name(fragment.on, &fragment.selection_set, query.query) { 14 | return Err(QueryValidationError::new(format!( 15 | "The `{}` fragment uses `{}` but does not select `__typename` on it. graphql-client cannot generate code for it. Please add `__typename` to the selection.", 16 | &fragment.name, 17 | type_id.name(query.schema), 18 | ))); 19 | } 20 | } 21 | 22 | let union_and_interface_field_selections = 23 | query 24 | .query 25 | .selections() 26 | .filter_map(|(selection_id, selection)| match selection { 27 | Selection::Field(field) => match query.schema.get_field(field.field_id).r#type.id { 28 | id @ TypeId::Interface(_) | id @ TypeId::Union(_) => { 29 | Some((selection_id, id, &field.selection_set)) 30 | } 31 | _ => None, 32 | }, 33 | _ => None, 34 | }); 35 | 36 | for selection in union_and_interface_field_selections { 37 | if !selection_set_contains_type_name(selection.1, selection.2, query.query) { 38 | return Err(QueryValidationError::new(format!( 39 | "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.", 40 | path = full_path_prefix(selection.0, query), 41 | selected_type = selection.1.name(query.schema) 42 | ))); 43 | } 44 | } 45 | 46 | Ok(()) 47 | } 48 | 49 | fn selection_set_contains_type_name( 50 | parent_type_id: TypeId, 51 | selection_set: &[SelectionId], 52 | query: &Query, 53 | ) -> bool { 54 | for id in selection_set { 55 | let selection = query.get_selection(*id); 56 | 57 | match selection { 58 | Selection::Typename => return true, 59 | Selection::FragmentSpread(fragment_id) => { 60 | let fragment = query.get_fragment(*fragment_id); 61 | if fragment.on == parent_type_id 62 | && selection_set_contains_type_name(fragment.on, &fragment.selection_set, query) 63 | { 64 | return true; 65 | } 66 | } 67 | _ => (), 68 | } 69 | } 70 | 71 | false 72 | } 73 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/schema/tests.rs: -------------------------------------------------------------------------------- 1 | mod extend_object; 2 | mod github; 3 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/schema/tests/extend_object.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::Schema; 2 | 3 | const SCHEMA_JSON: &str = include_str!("extend_object_schema.json"); 4 | const SCHEMA_GRAPHQL: &str = include_str!("extend_object_schema.graphql"); 5 | 6 | #[test] 7 | fn ast_from_graphql_and_json_produce_the_same_schema() { 8 | let json: graphql_introspection_query::introspection_response::IntrospectionResponse = 9 | serde_json::from_str(SCHEMA_JSON).unwrap(); 10 | let graphql_parser_schema = graphql_parser::parse_schema(SCHEMA_GRAPHQL) 11 | .unwrap() 12 | .into_static(); 13 | let mut json = Schema::from(json); 14 | let mut gql = Schema::from(graphql_parser_schema); 15 | 16 | assert!(vecs_match(&json.stored_scalars, &gql.stored_scalars)); 17 | 18 | // Root objects 19 | { 20 | assert_eq!( 21 | json.get_object(json.query_type()).name, 22 | gql.get_object(gql.query_type()).name 23 | ); 24 | assert_eq!( 25 | json.mutation_type().map(|t| &json.get_object(t).name), 26 | gql.mutation_type().map(|t| &gql.get_object(t).name), 27 | "Mutation types don't match." 28 | ); 29 | assert_eq!( 30 | json.subscription_type().map(|t| &json.get_object(t).name), 31 | gql.subscription_type().map(|t| &gql.get_object(t).name), 32 | "Subscription types don't match." 33 | ); 34 | } 35 | 36 | // Objects 37 | { 38 | let mut json_stored_objects: Vec<_> = json 39 | .stored_objects 40 | .drain(..) 41 | .filter(|obj| !obj.name.starts_with("__")) 42 | .collect(); 43 | 44 | assert_eq!( 45 | json_stored_objects.len(), 46 | gql.stored_objects.len(), 47 | "Objects count matches." 48 | ); 49 | 50 | json_stored_objects.sort_by(|a, b| a.name.cmp(&b.name)); 51 | gql.stored_objects.sort_by(|a, b| a.name.cmp(&b.name)); 52 | 53 | for (j, g) in json_stored_objects 54 | .iter_mut() 55 | .filter(|obj| !obj.name.starts_with("__")) 56 | .zip(gql.stored_objects.iter_mut()) 57 | { 58 | assert_eq!(j.name, g.name); 59 | assert_eq!( 60 | j.implements_interfaces.len(), 61 | g.implements_interfaces.len(), 62 | "{}", 63 | j.name 64 | ); 65 | assert_eq!(j.fields.len(), g.fields.len(), "{}", j.name); 66 | } 67 | } 68 | 69 | // Unions 70 | { 71 | assert_eq!(json.stored_unions.len(), gql.stored_unions.len()); 72 | 73 | json.stored_unions.sort_by(|a, b| a.name.cmp(&b.name)); 74 | gql.stored_unions.sort_by(|a, b| a.name.cmp(&b.name)); 75 | 76 | for (json, gql) in json.stored_unions.iter().zip(gql.stored_unions.iter()) { 77 | assert_eq!(json.variants.len(), gql.variants.len()); 78 | } 79 | } 80 | 81 | // Interfaces 82 | { 83 | assert_eq!(json.stored_interfaces.len(), gql.stored_interfaces.len()); 84 | 85 | json.stored_interfaces.sort_by(|a, b| a.name.cmp(&b.name)); 86 | gql.stored_interfaces.sort_by(|a, b| a.name.cmp(&b.name)); 87 | 88 | for (json, gql) in json 89 | .stored_interfaces 90 | .iter() 91 | .zip(gql.stored_interfaces.iter()) 92 | { 93 | assert_eq!(json.fields.len(), gql.fields.len()); 94 | } 95 | } 96 | 97 | // Input objects 98 | { 99 | json.stored_enums = json 100 | .stored_enums 101 | .drain(..) 102 | .filter(|enm| !enm.name.starts_with("__")) 103 | .collect(); 104 | assert_eq!(json.stored_inputs.len(), gql.stored_inputs.len()); 105 | 106 | json.stored_inputs.sort_by(|a, b| a.name.cmp(&b.name)); 107 | gql.stored_inputs.sort_by(|a, b| a.name.cmp(&b.name)); 108 | 109 | for (json, gql) in json.stored_inputs.iter().zip(gql.stored_inputs.iter()) { 110 | assert_eq!(json.fields.len(), gql.fields.len()); 111 | } 112 | } 113 | 114 | // Enums 115 | { 116 | assert_eq!(json.stored_enums.len(), gql.stored_enums.len()); 117 | 118 | json.stored_enums.sort_by(|a, b| a.name.cmp(&b.name)); 119 | gql.stored_enums.sort_by(|a, b| a.name.cmp(&b.name)); 120 | 121 | for (json, gql) in json.stored_enums.iter().zip(gql.stored_enums.iter()) { 122 | assert_eq!(json.variants.len(), gql.variants.len()); 123 | } 124 | } 125 | } 126 | 127 | fn vecs_match(a: &[T], b: &[T]) -> bool { 128 | a.len() == b.len() && a.iter().all(|a| b.iter().any(|b| a == b)) 129 | } 130 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/schema/tests/extend_object_schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | } 4 | 5 | type Query { 6 | foo: String 7 | } 8 | 9 | extend type Query { 10 | bar: Int 11 | } 12 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/schema/tests/github.rs: -------------------------------------------------------------------------------- 1 | use crate::schema::Schema; 2 | 3 | const SCHEMA_JSON: &str = include_str!("github_schema.json"); 4 | const SCHEMA_GRAPHQL: &str = include_str!("github_schema.graphql"); 5 | 6 | #[test] 7 | fn ast_from_graphql_and_json_produce_the_same_schema() { 8 | let json: graphql_introspection_query::introspection_response::IntrospectionResponse = 9 | serde_json::from_str(SCHEMA_JSON).unwrap(); 10 | let graphql_parser_schema = graphql_parser::parse_schema(SCHEMA_GRAPHQL) 11 | .unwrap() 12 | .into_static(); 13 | let mut json = Schema::from(json); 14 | let mut gql = Schema::from(graphql_parser_schema); 15 | 16 | assert!(vecs_match(&json.stored_scalars, &gql.stored_scalars)); 17 | 18 | // Root objects 19 | { 20 | assert_eq!( 21 | json.get_object(json.query_type()).name, 22 | gql.get_object(gql.query_type()).name 23 | ); 24 | assert_eq!( 25 | json.mutation_type().map(|t| &json.get_object(t).name), 26 | gql.mutation_type().map(|t| &gql.get_object(t).name), 27 | "Mutation types don't match." 28 | ); 29 | assert_eq!( 30 | json.subscription_type().map(|t| &json.get_object(t).name), 31 | gql.subscription_type().map(|t| &gql.get_object(t).name), 32 | "Subscription types don't match." 33 | ); 34 | } 35 | 36 | // Objects 37 | { 38 | let mut json_stored_objects: Vec<_> = json 39 | .stored_objects 40 | .drain(..) 41 | .filter(|obj| !obj.name.starts_with("__")) 42 | .collect(); 43 | 44 | assert_eq!( 45 | json_stored_objects.len(), 46 | gql.stored_objects.len(), 47 | "Objects count matches." 48 | ); 49 | 50 | json_stored_objects.sort_by(|a, b| a.name.cmp(&b.name)); 51 | gql.stored_objects.sort_by(|a, b| a.name.cmp(&b.name)); 52 | 53 | for (j, g) in json_stored_objects 54 | .iter_mut() 55 | .filter(|obj| !obj.name.starts_with("__")) 56 | .zip(gql.stored_objects.iter_mut()) 57 | { 58 | assert_eq!(j.name, g.name); 59 | assert_eq!( 60 | j.implements_interfaces.len(), 61 | g.implements_interfaces.len(), 62 | "{}", 63 | j.name 64 | ); 65 | assert_eq!(j.fields.len(), g.fields.len(), "{}", j.name); 66 | } 67 | } 68 | 69 | // Unions 70 | { 71 | assert_eq!(json.stored_unions.len(), gql.stored_unions.len()); 72 | 73 | json.stored_unions.sort_by(|a, b| a.name.cmp(&b.name)); 74 | gql.stored_unions.sort_by(|a, b| a.name.cmp(&b.name)); 75 | 76 | for (json, gql) in json.stored_unions.iter().zip(gql.stored_unions.iter()) { 77 | assert_eq!(json.variants.len(), gql.variants.len()); 78 | } 79 | } 80 | 81 | // Interfaces 82 | { 83 | assert_eq!(json.stored_interfaces.len(), gql.stored_interfaces.len()); 84 | 85 | json.stored_interfaces.sort_by(|a, b| a.name.cmp(&b.name)); 86 | gql.stored_interfaces.sort_by(|a, b| a.name.cmp(&b.name)); 87 | 88 | for (json, gql) in json 89 | .stored_interfaces 90 | .iter() 91 | .zip(gql.stored_interfaces.iter()) 92 | { 93 | assert_eq!(json.fields.len(), gql.fields.len()); 94 | } 95 | } 96 | 97 | // Input objects 98 | { 99 | json.stored_enums = json 100 | .stored_enums 101 | .drain(..) 102 | .filter(|enm| !enm.name.starts_with("__")) 103 | .collect(); 104 | assert_eq!(json.stored_inputs.len(), gql.stored_inputs.len()); 105 | 106 | json.stored_inputs.sort_by(|a, b| a.name.cmp(&b.name)); 107 | gql.stored_inputs.sort_by(|a, b| a.name.cmp(&b.name)); 108 | 109 | for (json, gql) in json.stored_inputs.iter().zip(gql.stored_inputs.iter()) { 110 | assert_eq!(json.fields.len(), gql.fields.len()); 111 | } 112 | } 113 | 114 | // Enums 115 | { 116 | assert_eq!(json.stored_enums.len(), gql.stored_enums.len()); 117 | 118 | json.stored_enums.sort_by(|a, b| a.name.cmp(&b.name)); 119 | gql.stored_enums.sort_by(|a, b| a.name.cmp(&b.name)); 120 | 121 | for (json, gql) in json.stored_enums.iter().zip(gql.stored_enums.iter()) { 122 | assert_eq!(json.variants.len(), gql.variants.len()); 123 | } 124 | } 125 | } 126 | 127 | fn vecs_match(a: &[T], b: &[T]) -> bool { 128 | a.len() == b.len() && a.iter().all(|a| b.iter().any(|b| a == b)) 129 | } 130 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/tests/foobars_query.graphql: -------------------------------------------------------------------------------- 1 | query FooBarsQuery { 2 | fooBars { 3 | fooBars { 4 | __typename 5 | ... on Foo { 6 | fooField 7 | } 8 | ... on Bar { 9 | barField 10 | } 11 | ... on FooBar { 12 | fooBarField 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/tests/foobars_schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | mutation: Mutation 4 | } 5 | 6 | directive @defer on FIELD 7 | 8 | type Query { 9 | fooBars: Self 10 | } 11 | 12 | type Self { 13 | fooBars: Result 14 | } 15 | 16 | union Result = Foo | Bar | FooBar 17 | 18 | type Foo { 19 | fooField: String! 20 | } 21 | 22 | type Bar { 23 | barField: String! 24 | } 25 | 26 | type FooBar { 27 | fooBarField: String! 28 | } 29 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/tests/keywords_query.graphql: -------------------------------------------------------------------------------- 1 | query searchQuery($criteria: extern!) { 2 | search { 3 | transactions(criteria: $searchID) { 4 | for 5 | status 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/tests/keywords_schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | mutation: Mutation 4 | } 5 | 6 | """ 7 | This directive allows results to be deferred during execution 8 | """ 9 | directive @defer on FIELD 10 | 11 | """ 12 | The top-level Query type. 13 | """ 14 | type Query { 15 | """ 16 | Keyword type 17 | """ 18 | search: Self 19 | } 20 | 21 | """ 22 | Keyword type 23 | """ 24 | type Self { 25 | """ 26 | A keyword variable name with a keyword-named input type 27 | """ 28 | transactions(struct: extern!): Result 29 | } 30 | 31 | """ 32 | Keyword type 33 | """ 34 | type Result { 35 | """ 36 | Keyword field. 37 | """ 38 | for: String 39 | """ 40 | dummy field with enum 41 | """ 42 | status: AnEnum 43 | } 44 | 45 | """ 46 | Keyword input 47 | """ 48 | input extern { 49 | """ 50 | A field 51 | """ 52 | id: crate 53 | } 54 | 55 | """ 56 | Input fields for searching for specific values. 57 | """ 58 | input crate { 59 | """ 60 | Keyword field. 61 | """ 62 | enum: String 63 | 64 | """ 65 | Keyword field. 66 | """ 67 | in: [String!] 68 | } 69 | 70 | """ 71 | Enum with keywords 72 | """ 73 | enum AnEnum { 74 | where 75 | self 76 | } 77 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use crate::{generate_module_token_stream_from_string, CodegenMode, GraphQLClientCodegenOptions}; 4 | 5 | const KEYWORDS_QUERY: &str = include_str!("keywords_query.graphql"); 6 | const KEYWORDS_SCHEMA_PATH: &str = "keywords_schema.graphql"; 7 | 8 | const FOOBARS_QUERY: &str = include_str!("foobars_query.graphql"); 9 | const FOOBARS_SCHEMA_PATH: &str = "foobars_schema.graphql"; 10 | 11 | fn build_schema_path(path: &str) -> PathBuf { 12 | std::env::current_dir() 13 | .unwrap() 14 | .join("src/tests") 15 | .join(path) 16 | } 17 | 18 | #[test] 19 | fn schema_with_keywords_works() { 20 | let query_string = KEYWORDS_QUERY; 21 | let schema_path = build_schema_path(KEYWORDS_SCHEMA_PATH); 22 | 23 | let options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); 24 | 25 | let generated_tokens = 26 | generate_module_token_stream_from_string(query_string, &schema_path, options) 27 | .expect("Generate keywords module"); 28 | 29 | let generated_code = generated_tokens.to_string(); 30 | 31 | // Parse generated code. All keywords should be correctly escaped. 32 | let r: syn::parse::Result = syn::parse2(generated_tokens); 33 | match r { 34 | Ok(_) => { 35 | // Rust keywords should be escaped / renamed now 36 | assert!(generated_code.contains("pub in_")); 37 | assert!(generated_code.contains("extern_")); 38 | } 39 | Err(e) => { 40 | panic!("Error: {}\n Generated content: {}\n", e, &generated_code); 41 | } 42 | }; 43 | } 44 | 45 | #[test] 46 | fn blended_custom_types_works() { 47 | let query_string = KEYWORDS_QUERY; 48 | let schema_path = build_schema_path(KEYWORDS_SCHEMA_PATH); 49 | 50 | let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); 51 | options.set_custom_response_type("external_crate::Transaction".to_string()); 52 | options.set_custom_variable_types(vec!["external_crate::ID".to_string()]); 53 | 54 | let generated_tokens = 55 | generate_module_token_stream_from_string(query_string, &schema_path, options) 56 | .expect("Generate keywords module"); 57 | 58 | let generated_code = generated_tokens.to_string(); 59 | 60 | // Parse generated code. Variables and returns should be replaced with custom types 61 | let r: syn::parse::Result = syn::parse2(generated_tokens); 62 | match r { 63 | Ok(_) => { 64 | // Variables and returns should be replaced with custom types 65 | assert!(generated_code.contains("pub type SearchQuerySearch = external_crate :: Transaction")); 66 | assert!(generated_code.contains("pub type extern_ = external_crate :: ID")); 67 | } 68 | Err(e) => { 69 | panic!("Error: {}\n Generated content: {}\n", e, &generated_code); 70 | } 71 | }; 72 | } 73 | 74 | #[test] 75 | fn fragments_other_variant_should_generate_unknown_other_variant() { 76 | let query_string = FOOBARS_QUERY; 77 | let schema_path = build_schema_path(FOOBARS_SCHEMA_PATH); 78 | 79 | let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); 80 | 81 | options.set_fragments_other_variant(true); 82 | 83 | let generated_tokens = 84 | generate_module_token_stream_from_string(query_string, &schema_path, options) 85 | .expect("Generate foobars module"); 86 | 87 | let generated_code = generated_tokens.to_string(); 88 | 89 | let r: syn::parse::Result = syn::parse2(generated_tokens); 90 | match r { 91 | Ok(_) => { 92 | // Rust keywords should be escaped / renamed now 93 | assert!(generated_code.contains("# [serde (other)] Unknown")); 94 | assert!(generated_code.contains("Unknown")); 95 | } 96 | Err(e) => { 97 | panic!("Error: {}\n Generated content: {}\n", e, &generated_code); 98 | } 99 | }; 100 | } 101 | 102 | #[test] 103 | fn fragments_other_variant_false_should_not_generate_unknown_other_variant() { 104 | let query_string = FOOBARS_QUERY; 105 | let schema_path = build_schema_path(FOOBARS_SCHEMA_PATH); 106 | 107 | let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); 108 | 109 | options.set_fragments_other_variant(false); 110 | 111 | let generated_tokens = 112 | generate_module_token_stream_from_string(query_string, &schema_path, options) 113 | .expect("Generate foobars module token stream"); 114 | 115 | let generated_code = generated_tokens.to_string(); 116 | 117 | let r: syn::parse::Result = syn::parse2(generated_tokens); 118 | match r { 119 | Ok(_) => { 120 | // Rust keywords should be escaped / renamed now 121 | assert!(!generated_code.contains("# [serde (other)] Unknown")); 122 | assert!(!generated_code.contains("Unknown")); 123 | } 124 | Err(e) => { 125 | panic!("Error: {}\n Generated content: {}\n", e, &generated_code); 126 | } 127 | }; 128 | } 129 | 130 | #[test] 131 | fn skip_serializing_none_should_generate_serde_skip_serializing() { 132 | let query_string = KEYWORDS_QUERY; 133 | let schema_path = build_schema_path(KEYWORDS_SCHEMA_PATH); 134 | 135 | let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); 136 | 137 | options.set_skip_serializing_none(true); 138 | 139 | let generated_tokens = 140 | generate_module_token_stream_from_string(query_string, &schema_path, options) 141 | .expect("Generate foobars module"); 142 | 143 | let generated_code = generated_tokens.to_string(); 144 | 145 | let r: syn::parse::Result = syn::parse2(generated_tokens); 146 | 147 | match r { 148 | Ok(_) => { 149 | println!("{}", generated_code); 150 | assert!(generated_code.contains("skip_serializing_if")); 151 | } 152 | Err(e) => { 153 | panic!("Error: {}\n Generated content: {}\n", e, &generated_code); 154 | } 155 | }; 156 | } 157 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/tests/star_wars_query.graphql: -------------------------------------------------------------------------------- 1 | query StarWarsQuery($episodeForHero: Episode!) { 2 | hero(episode: $episodeForHero) { 3 | name 4 | __typename 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/tests/star_wars_schema.graphql: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/apollographql/starwars-server/blob/master/data/swapiSchema.js 2 | 3 | schema { 4 | query: Query 5 | mutation: Mutation 6 | subscription: Subscription 7 | } 8 | 9 | # The query type, represents all of the entry points into our object graph 10 | type Query { 11 | hero(episode: Episode): Character 12 | reviews(episode: Episode!): [Review] 13 | search(text: String): [SearchResult] 14 | character(id: ID!): Character 15 | droid(id: ID!): Droid 16 | human(id: ID!): Human 17 | starship(id: ID!): Starship 18 | } 19 | 20 | # The mutation type, represents all updates we can make to our data 21 | type Mutation { 22 | createReview(episode: Episode, review: ReviewInput!): Review 23 | } 24 | 25 | # The subscription type, represents all subscriptions we can make to our data 26 | type Subscription { 27 | reviewAdded(episode: Episode): Review 28 | } 29 | 30 | # The episodes in the Star Wars trilogy 31 | enum Episode { 32 | # Star Wars Episode IV: A New Hope, released in 1977. 33 | NEWHOPE 34 | # Star Wars Episode V: The Empire Strikes Back, released in 1980. 35 | EMPIRE 36 | # Star Wars Episode VI: Return of the Jedi, released in 1983. 37 | JEDI 38 | } 39 | 40 | # A character from the Star Wars universe 41 | interface Character { 42 | # The ID of the character 43 | id: ID! 44 | # The name of the character 45 | name: String! 46 | # The friends of the character, or an empty list if they have none 47 | friends: [Character] 48 | # The friends of the character exposed as a connection with edges 49 | friendsConnection(first: Int, after: ID): FriendsConnection! 50 | # The movies this character appears in 51 | appearsIn: [Episode]! 52 | } 53 | 54 | # Units of height 55 | enum LengthUnit { 56 | # The standard unit around the world 57 | METER 58 | # Primarily used in the United States 59 | FOOT 60 | } 61 | 62 | # A humanoid creature from the Star Wars universe 63 | type Human implements Character { 64 | # The ID of the human 65 | id: ID! 66 | # What this human calls themselves 67 | name: String! 68 | # The home planet of the human, or null if unknown 69 | homePlanet: String 70 | # Height in the preferred unit, default is meters 71 | height(unit: LengthUnit = METER): Float 72 | # Mass in kilograms, or null if unknown 73 | mass: Float 74 | # This human's friends, or an empty list if they have none 75 | friends: [Character] 76 | # The friends of the human exposed as a connection with edges 77 | friendsConnection(first: Int, after: ID): FriendsConnection! 78 | # The movies this human appears in 79 | appearsIn: [Episode]! 80 | # A list of starships this person has piloted, or an empty list if none 81 | starships: [Starship] 82 | } 83 | 84 | # An autonomous mechanical character in the Star Wars universe 85 | type Droid implements Character { 86 | # The ID of the droid 87 | id: ID! 88 | # What others call this droid 89 | name: String! 90 | # This droid's friends, or an empty list if they have none 91 | friends: [Character] 92 | # The friends of the droid exposed as a connection with edges 93 | friendsConnection(first: Int, after: ID): FriendsConnection! 94 | # The movies this droid appears in 95 | appearsIn: [Episode]! 96 | # This droid's primary function 97 | primaryFunction: String 98 | } 99 | 100 | # A connection object for a character's friends 101 | type FriendsConnection { 102 | # The total number of friends 103 | totalCount: Int 104 | # The edges for each of the character's friends. 105 | edges: [FriendsEdge] 106 | # A list of the friends, as a convenience when edges are not needed. 107 | friends: [Character] 108 | # Information for paginating this connection 109 | pageInfo: PageInfo! 110 | } 111 | 112 | # An edge object for a character's friends 113 | type FriendsEdge { 114 | # A cursor used for pagination 115 | cursor: ID! 116 | # The character represented by this friendship edge 117 | node: Character 118 | } 119 | 120 | # Information for paginating this connection 121 | type PageInfo { 122 | startCursor: ID 123 | endCursor: ID 124 | hasNextPage: Boolean! 125 | } 126 | 127 | # Represents a review for a movie 128 | type Review { 129 | # The movie 130 | episode: Episode 131 | # The number of stars this review gave, 1-5 132 | stars: Int! 133 | # Comment about the movie 134 | commentary: String 135 | } 136 | 137 | # The input object sent when someone is creating a new review 138 | input ReviewInput { 139 | # 0-5 stars 140 | stars: Int! 141 | # Comment about the movie, optional 142 | commentary: String 143 | # Favorite color, optional 144 | favorite_color: ColorInput 145 | } 146 | 147 | # The input object sent when passing in a color 148 | input ColorInput { 149 | red: Int! 150 | green: Int! 151 | blue: Int! 152 | } 153 | 154 | type Starship { 155 | # The ID of the starship 156 | id: ID! 157 | # The name of the starship 158 | name: String! 159 | # Length of the starship, along the longest axis 160 | length(unit: LengthUnit = METER): Float 161 | coordinates: [[Float!]!] 162 | } 163 | 164 | union SearchResult = Human | Droid | Starship 165 | -------------------------------------------------------------------------------- /graphql_client_codegen/src/type_qualifiers.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Debug, PartialEq, Hash)] 2 | pub(crate) enum GraphqlTypeQualifier { 3 | Required, 4 | List, 5 | } 6 | 7 | impl GraphqlTypeQualifier { 8 | pub(crate) fn is_required(&self) -> bool { 9 | *self == GraphqlTypeQualifier::Required 10 | } 11 | } 12 | 13 | pub fn graphql_parser_depth<'doc, T>(schema_type: &graphql_parser::schema::Type<'doc, T>) -> usize 14 | where 15 | T: graphql_parser::query::Text<'doc>, 16 | { 17 | match schema_type { 18 | graphql_parser::schema::Type::ListType(inner) => 1 + graphql_parser_depth(inner), 19 | graphql_parser::schema::Type::NonNullType(inner) => 1 + graphql_parser_depth(inner), 20 | graphql_parser::schema::Type::NamedType(_) => 0, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /graphql_query_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphql_query_derive" 3 | version = "0.14.0" 4 | authors = ["Tom Houlé "] 5 | description = "Utility crate for graphql_client" 6 | license = "Apache-2.0 OR MIT" 7 | repository = "https://github.com/graphql-rust/graphql-client" 8 | edition = "2018" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [dependencies] 14 | syn = { version = "^2.0", features = ["extra-traits"] } 15 | proc-macro2 = { version = "^1.0", features = [] } 16 | graphql_client_codegen = { path = "../graphql_client_codegen/", version = "0.14.0" } 17 | -------------------------------------------------------------------------------- /graphql_query_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | /// Derive-related code. This will be moved into graphql_query_derive. 4 | mod attributes; 5 | 6 | use graphql_client_codegen::{ 7 | generate_module_token_stream, CodegenMode, GraphQLClientCodegenOptions, 8 | }; 9 | use std::{ 10 | env, 11 | path::{Path, PathBuf}, 12 | }; 13 | 14 | use proc_macro2::TokenStream; 15 | 16 | #[proc_macro_derive(GraphQLQuery, attributes(graphql))] 17 | pub fn derive_graphql_query(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 18 | match graphql_query_derive_inner(input) { 19 | Ok(ts) => ts, 20 | Err(err) => err.to_compile_error().into(), 21 | } 22 | } 23 | 24 | fn graphql_query_derive_inner( 25 | input: proc_macro::TokenStream, 26 | ) -> Result { 27 | let input = TokenStream::from(input); 28 | let ast = syn::parse2(input)?; 29 | let (query_path, schema_path) = build_query_and_schema_path(&ast)?; 30 | let options = build_graphql_client_derive_options(&ast, query_path.clone())?; 31 | 32 | generate_module_token_stream(query_path, &schema_path, options) 33 | .map(Into::into) 34 | .map_err(|err| { 35 | syn::Error::new_spanned( 36 | ast, 37 | format!("Failed to generate GraphQLQuery impl: {}", err), 38 | ) 39 | }) 40 | } 41 | 42 | fn build_query_and_schema_path(input: &syn::DeriveInput) -> Result<(PathBuf, PathBuf), syn::Error> { 43 | let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_err| { 44 | syn::Error::new_spanned( 45 | input, 46 | "Error checking that the CARGO_MANIFEST_DIR env variable is defined.", 47 | ) 48 | })?; 49 | 50 | let query_path = attributes::extract_attr(input, "query_path")?; 51 | let query_path = format!("{}/{}", cargo_manifest_dir, query_path); 52 | let query_path = Path::new(&query_path).to_path_buf(); 53 | let schema_path = attributes::extract_attr(input, "schema_path")?; 54 | let schema_path = Path::new(&cargo_manifest_dir).join(schema_path); 55 | Ok((query_path, schema_path)) 56 | } 57 | 58 | fn build_graphql_client_derive_options( 59 | input: &syn::DeriveInput, 60 | query_path: PathBuf, 61 | ) -> Result { 62 | let variables_derives = attributes::extract_attr(input, "variables_derives").ok(); 63 | let response_derives = attributes::extract_attr(input, "response_derives").ok(); 64 | let custom_scalars_module = attributes::extract_attr(input, "custom_scalars_module").ok(); 65 | let extern_enums = attributes::extract_attr_list(input, "extern_enums").ok(); 66 | let fragments_other_variant: bool = attributes::extract_fragments_other_variant(input); 67 | let skip_serializing_none: bool = attributes::extract_skip_serializing_none(input); 68 | let custom_variable_types = attributes::extract_attr_list(input, "variable_types").ok(); 69 | let custom_response_type = attributes::extract_attr(input, "response_type").ok(); 70 | 71 | let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Derive); 72 | options.set_query_file(query_path); 73 | options.set_fragments_other_variant(fragments_other_variant); 74 | options.set_skip_serializing_none(skip_serializing_none); 75 | 76 | if let Some(variables_derives) = variables_derives { 77 | options.set_variables_derives(variables_derives); 78 | }; 79 | 80 | if let Some(response_derives) = response_derives { 81 | options.set_response_derives(response_derives); 82 | }; 83 | 84 | // The user can determine what to do about deprecations. 85 | if let Ok(deprecation_strategy) = attributes::extract_deprecation_strategy(input) { 86 | options.set_deprecation_strategy(deprecation_strategy); 87 | }; 88 | 89 | // The user can specify the normalization strategy. 90 | if let Ok(normalization) = attributes::extract_normalization(input) { 91 | options.set_normalization(normalization); 92 | }; 93 | 94 | // The user can give a path to a module that provides definitions for the custom scalars. 95 | if let Some(custom_scalars_module) = custom_scalars_module { 96 | let custom_scalars_module = syn::parse_str(&custom_scalars_module)?; 97 | 98 | options.set_custom_scalars_module(custom_scalars_module); 99 | } 100 | 101 | // The user can specify a list of enums types that are defined externally, rather than generated by this library 102 | if let Some(extern_enums) = extern_enums { 103 | options.set_extern_enums(extern_enums); 104 | } 105 | 106 | if let Some(custom_variable_types) = custom_variable_types { 107 | options.set_custom_variable_types(custom_variable_types); 108 | } 109 | 110 | if let Some(custom_response_type) = custom_response_type { 111 | options.set_custom_response_type(custom_response_type); 112 | } 113 | 114 | options.set_struct_ident(input.ident.clone()); 115 | options.set_module_visibility(input.vis.clone()); 116 | options.set_operation_name(input.ident.to_string()); 117 | options.set_serde_path(syn::parse_quote!(graphql_client::_private::serde)); 118 | 119 | Ok(options) 120 | } 121 | --------------------------------------------------------------------------------