├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md └── workflows │ ├── ci.yml │ ├── code-coverage.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── Dockerfile-examples ├── Dockerfile-standalone-demo ├── LICENSE ├── README-cn.md ├── README.md ├── benchmark.lua ├── benchmark.sh ├── crates ├── handler │ ├── Cargo.toml │ └── src │ │ ├── constants.rs │ │ ├── executor.rs │ │ ├── fetcher.rs │ │ ├── handler.rs │ │ ├── introspection │ │ ├── enum_value.rs │ │ ├── field.rs │ │ ├── input_value.rs │ │ ├── mod.rs │ │ ├── resolver.rs │ │ ├── root.rs │ │ ├── schema.rs │ │ └── type.rs │ │ ├── lib.rs │ │ ├── metrics.rs │ │ ├── playground.html │ │ ├── service_route.rs │ │ ├── shared_route_table.rs │ │ └── websocket │ │ ├── controller.rs │ │ ├── grouped_stream.rs │ │ ├── mod.rs │ │ ├── protocol.rs │ │ └── server.rs ├── planner │ ├── Cargo.toml │ ├── src │ │ ├── builder.rs │ │ ├── lib.rs │ │ ├── plan.rs │ │ ├── request.rs │ │ ├── response.rs │ │ └── types.rs │ └── tests │ │ ├── fragment_on_interface.txt │ │ ├── fragment_spread.txt │ │ ├── inline_fragment.txt │ │ ├── mutation.txt │ │ ├── node_fragments.txt │ │ ├── possible_interface.txt │ │ ├── possible_union.txt │ │ ├── query.txt │ │ ├── subscribe.txt │ │ ├── test.graphql │ │ ├── test.rs │ │ └── variables.txt ├── schema │ ├── Cargo.toml │ └── src │ │ ├── builtin.graphql │ │ ├── composed_schema.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── type_ext.rs │ │ └── value_ext.rs └── validation │ ├── Cargo.toml │ └── src │ ├── error.rs │ ├── lib.rs │ ├── rules │ ├── arguments_of_correct_type.rs │ ├── default_values_of_correct_type.rs │ ├── fields_on_correct_type.rs │ ├── fragments_on_composite_types.rs │ ├── known_argument_names.rs │ ├── known_directives.rs │ ├── known_fragment_names.rs │ ├── known_type_names.rs │ ├── mod.rs │ ├── no_fragment_cycles.rs │ ├── no_undefined_variables.rs │ ├── no_unused_fragments.rs │ ├── no_unused_variables.rs │ ├── overlapping_fields_can_be_merged.rs │ ├── possible_fragment_spreads.rs │ ├── provided_non_null_arguments.rs │ ├── scalar_leafs.rs │ ├── unique_argument_names.rs │ ├── unique_variable_names.rs │ ├── variables_are_input_types.rs │ └── variables_in_allowed_position.rs │ ├── suggestion.rs │ ├── test_harness.graphql │ ├── test_harness.rs │ ├── utils.rs │ └── visitor.rs ├── examples ├── accounts.rs ├── builtin_scalar_bug │ ├── bug.rs │ └── config.toml ├── helm │ ├── .helmignore │ ├── Chart.yaml │ ├── config.toml │ ├── templates │ │ ├── examples.yaml │ │ ├── graphgate.yaml │ │ └── jaeger.yaml │ └── values.yaml ├── products.rs └── reviews.rs ├── rustfmt.toml └── src ├── config.rs ├── k8s.rs ├── main.rs └── options.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Bug Report' 3 | about: 'Report a new bug' 4 | title: '' 5 | labels: bug 6 | --- 7 | 8 | ## Expected Behavior 9 | 10 | 11 | ## Actual Behavior 12 | 13 | 14 | ## Steps to Reproduce the Problem 15 | 16 | 1. 17 | 2. 18 | 3. 19 | 20 | ## Specifications 21 | 22 | - Version: 23 | - Platform: 24 | - Subsystem: -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Feature Request' 3 | about: 'Report a new feature to be implemented' 4 | title: '<Title>' 5 | labels: enhancement 6 | --- 7 | 8 | ## Description of the feature 9 | 10 | 11 | ## Code example (if possible) -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 'Question' 3 | about: 'If something needs clarification' 4 | title: '<Title>' 5 | labels: question 6 | --- 7 | 8 | <!-- What is your question? Please be as specific as possible! --> -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - uses: actions-rs/toolchain@v1 18 | with: 19 | toolchain: stable 20 | override: true 21 | components: clippy, rustfmt 22 | - name: Check format 23 | run: cargo fmt --all -- --check 24 | - name: Check with clippy 25 | run: cargo clippy --all 26 | - name: Build 27 | run: cargo build --all --verbose 28 | - name: Run tests 29 | run: cargo test --all --verbose 30 | -------------------------------------------------------------------------------- /.github/workflows/code-coverage.yml: -------------------------------------------------------------------------------- 1 | name: Code Coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | cover: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions-rs/toolchain@v1 14 | with: 15 | toolchain: stable 16 | override: true 17 | - name: Install libsqlite3-dev 18 | run: | 19 | sudo apt-get update 20 | sudo apt-get install -y libsqlite3-dev 21 | - name: Run cargo-tarpaulin 22 | uses: actions-rs/tarpaulin@v0.1 23 | with: 24 | version: '0.14.3' 25 | args: --out Xml --all 26 | - name: Upload to codecov.io 27 | uses: codecov/codecov-action@v1.0.2 28 | with: 29 | token: ${{secrets.CODECOV_TOKEN}} 30 | - name: Archive code coverage results 31 | uses: actions/upload-artifact@v1 32 | with: 33 | name: code-coverage-report 34 | path: cobertura.xml 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - release 7 | 8 | jobs: 9 | graphgate-docker: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Get version 15 | run: echo PACKAGE_VERSION=$(sed -nE 's/^\s*version = "(.*?)"/\1/p' Cargo.toml) >> $GITHUB_ENV 16 | - 17 | name: Login to DockerHub 18 | uses: docker/login-action@v1 19 | with: 20 | username: ${{ secrets.DOCKER_USER }} 21 | password: ${{ secrets.DOCKER_TOKEN }} 22 | - name: Build and push 23 | uses: docker/build-push-action@v2 24 | with: 25 | push: true 26 | context: . 27 | tags: | 28 | scott829/graphgate:${{ env.PACKAGE_VERSION }} 29 | scott829/graphgate:latest 30 | - 31 | name: Image digest 32 | run: echo ${{ steps.docker_build.outputs.digest }} 33 | 34 | examples-docker: 35 | runs-on: ubuntu-latest 36 | needs: graphgate-docker 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@v2 40 | - 41 | name: Login to DockerHub 42 | uses: docker/login-action@v1 43 | with: 44 | username: ${{ secrets.DOCKER_USER }} 45 | password: ${{ secrets.DOCKER_TOKEN }} 46 | - name: Build and push ${{ matrix.package.name }} 47 | uses: docker/build-push-action@v2 48 | with: 49 | push: true 50 | context: . 51 | file: Dockerfile-examples 52 | tags: scott829/graphgate-examples:latest 53 | - name: Deploy to Kubernetes 54 | uses: WyriHaximus/github-action-helm3@v2 55 | with: 56 | kubeconfig: '${{ secrets.K8S_CONFIG }}' 57 | exec: | 58 | helm uninstall -n graphgate graphgate 59 | helm upgrade --create-namespace -i -n graphgate graphgate examples/helm 60 | 61 | standalone-demo-docker: 62 | runs-on: ubuntu-latest 63 | steps: 64 | - name: Checkout 65 | uses: actions/checkout@v2 66 | - 67 | name: Login to DockerHub 68 | uses: docker/login-action@v1 69 | with: 70 | username: ${{ secrets.DOCKER_USER }} 71 | password: ${{ secrets.DOCKER_TOKEN }} 72 | - name: Build and push ${{ matrix.package.name }} 73 | uses: docker/build-push-action@v2 74 | with: 75 | push: true 76 | context: . 77 | file: Dockerfile-standalone-demo 78 | tags: scott829/graphgate-standalone-demo:latest 79 | 80 | publish: 81 | runs-on: ubuntu-latest 82 | strategy: 83 | fail-fast: false 84 | max-parallel: 1 85 | matrix: 86 | package: 87 | - name: graphgate-schema 88 | registryName: graphgate-schema 89 | path: crates/schema 90 | - name: graphgate-validation 91 | registryName: graphgate-validation 92 | path: crates/validation 93 | - name: graphgate-planner 94 | registryName: graphgate-planner 95 | path: crates/planner 96 | - name: graphgate-handler 97 | registryName: graphgate-handler 98 | path: crates/handler 99 | - name: graphgate 100 | registryName: graphgate 101 | path: . 102 | steps: 103 | - name: Checkout 104 | uses: actions/checkout@v2 105 | - name: get version 106 | working-directory: ${{ matrix.package.path }} 107 | run: echo PACKAGE_VERSION=$(sed -nE 's/^\s*version = "(.*?)"/\1/p' Cargo.toml) >> $GITHUB_ENV 108 | - name: check published version 109 | run: echo PUBLISHED_VERSION=$(cargo search ${{ matrix.package.registryName }} --limit 1 | sed -nE 's/^[^"]*"//; s/".*//1p' -) >> $GITHUB_ENV 110 | - name: cargo login 111 | if: env.PACKAGE_VERSION != env.PUBLISHED_VERSION 112 | run: cargo login ${{ secrets.CRATES_TOKEN }} 113 | - name: cargo package 114 | if: env.PACKAGE_VERSION != env.PUBLISHED_VERSION 115 | working-directory: ${{ matrix.package.path }} 116 | run: | 117 | cargo package 118 | echo "We will publish:" $PACKAGE_VERSION 119 | echo "This is current latest:" $PUBLISHED_VERSION 120 | - name: Publish ${{ matrix.package.name }} 121 | if: env.PACKAGE_VERSION != env.PUBLISHED_VERSION 122 | working-directory: ${{ matrix.package.path }} 123 | run: | 124 | echo "# Cargo Publish" 125 | cargo publish --no-verify 126 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphgate" 3 | version = "0.5.1" 4 | authors = ["Sunli <scott_s829@163.com>"] 5 | edition = "2018" 6 | description = "GraphGate is Apollo Federation implemented in Rust" 7 | license = "MIT/Apache-2.0" 8 | homepage = "https://github.com/async-graphql/graphgate" 9 | repository = "https://github.com/async-graphql/graphgate" 10 | keywords = ["gateway", "graphql", "federation"] 11 | readme = "README.md" 12 | 13 | [dependencies] 14 | graphgate-handler = { version = "0.5.0", path = "./crates/handler" } 15 | 16 | serde = { version = "1.0.133", features = ["derive"] } 17 | anyhow = "1.0.52" 18 | structopt = "0.3.25" 19 | kube = { version = "0.66.0", features = ["derive", "client", "rustls-tls"], default-features = false } 20 | k8s-openapi = { version = "0.13.1", features = ["v1_22"], default-features = false } 21 | tokio = { version = "1.15.0", features = ["rt-multi-thread", "time", "macros", "sync", "signal"] } 22 | warp = { version = "0.3.2", features = ["compression"] } 23 | toml = "0.5.8" 24 | futures-util = "0.3.19" 25 | tracing = "0.1.29" 26 | tracing-subscriber = { version = "0.3.6", features = ["env-filter"] } 27 | opentelemetry = { version = "0.16.0", features = ["rt-tokio", "metrics"] } 28 | opentelemetry-jaeger = { version = "0.15.0", features = ["rt-tokio"] } 29 | opentelemetry-prometheus = "0.9.0" 30 | prometheus = "0.12.0" 31 | 32 | [target.'cfg(all(target_env = "musl", target_pointer_width = "64"))'.dependencies.jemallocator] 33 | version = "0.3.2" 34 | 35 | [dev-dependencies] 36 | async-graphql = { version = "3.0.24", features = ["apollo_tracing"] } 37 | async-graphql-warp = "3.0.24" 38 | fastrand = "1.6.0" 39 | async-stream = "0.3.2" 40 | futures-util = "0.3.19" 41 | 42 | [[example]] 43 | name = "builtin_scalar_bug" 44 | path = "./examples/builtin_scalar_bug/bug.rs" 45 | 46 | [workspace] 47 | members = [ 48 | "crates/schema", 49 | "crates/planner", 50 | "crates/validation", 51 | "crates/handler", 52 | ] 53 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ### 2 | # Builder 3 | ### 4 | FROM rust:latest as builder 5 | 6 | RUN rustup target add x86_64-unknown-linux-musl 7 | RUN apt update && apt install -y musl-tools musl-dev 8 | RUN update-ca-certificates 9 | 10 | ENV USER=graphgate 11 | ENV UID=10001 12 | 13 | 14 | RUN adduser \ 15 | --disabled-password \ 16 | --gecos "" \ 17 | --home "/nonexistent" \ 18 | --shell "/sbin/nologin" \ 19 | --no-create-home \ 20 | --uid "${UID}" \ 21 | "${USER}" 22 | 23 | WORKDIR /graphgate 24 | 25 | COPY ./ . 26 | 27 | RUN cargo build --target x86_64-unknown-linux-musl --release 28 | 29 | ### 30 | # Final Image 31 | ### 32 | 33 | FROM scratch 34 | 35 | COPY --from=builder /etc/passwd /etc/passwd 36 | COPY --from=builder /etc/group /etc/group 37 | 38 | WORKDIR /graphgate 39 | 40 | COPY --from=builder /graphgate/target/x86_64-unknown-linux-musl/release/graphgate ./ 41 | 42 | USER graphgate:graphgate 43 | 44 | ENTRYPOINT [ "/graphgate/graphgate" ] 45 | -------------------------------------------------------------------------------- /Dockerfile-examples: -------------------------------------------------------------------------------- 1 | FROM rust:1.50 as builder 2 | RUN apt-get update && apt-get install -y libssl-dev 3 | 4 | COPY . /tmp 5 | WORKDIR /tmp 6 | RUN cargo build --examples --release 7 | 8 | FROM ubuntu:18.04 9 | RUN apt-get update && apt-get install -y libssl-dev 10 | COPY --from=builder /tmp/target/release/examples/accounts /usr/bin/accounts 11 | COPY --from=builder /tmp/target/release/examples/products /usr/bin/products 12 | COPY --from=builder /tmp/target/release/examples/reviews /usr/bin/reviews 13 | -------------------------------------------------------------------------------- /Dockerfile-standalone-demo: -------------------------------------------------------------------------------- 1 | FROM rust:1.50 as builder 2 | RUN apt-get update && apt-get install -y libssl-dev 3 | 4 | COPY . /tmp 5 | WORKDIR /tmp 6 | RUN cargo build --bins --examples --release 7 | 8 | FROM ubuntu:18.04 9 | RUN apt-get update && apt-get install -y libssl-dev 10 | COPY --from=builder /tmp/target/release/graphgate /usr/bin/graphgate 11 | COPY --from=builder /tmp/target/release/examples/accounts /usr/bin/accounts 12 | COPY --from=builder /tmp/target/release/examples/products /usr/bin/products 13 | COPY --from=builder /tmp/target/release/examples/reviews /usr/bin/reviews 14 | EXPOSE 8000 15 | 16 | RUN echo "\n\ 17 | bind = \"0.0.0.0:8000\"\n\ 18 | \n\ 19 | [[services]]\n\ 20 | name = \"accounts\"\n\ 21 | addr = \"127.0.0.1:8001\"\n\ 22 | \n\ 23 | [[services]]\n\ 24 | name = \"products\"\n\ 25 | addr = \"127.0.0.1:8002\"\n\ 26 | \n\ 27 | [[services]]\n\ 28 | name = \"reviews\"\n\ 29 | addr = \"127.0.0.1:8003\"\n\ 30 | " > /etc/graphgate.conf 31 | 32 | RUN echo "\n\ 33 | accounts&\n\ 34 | products&\n\ 35 | reviews&\n\ 36 | sleep 1\n\ 37 | graphgate /etc/graphgate.conf\n\ 38 | " > /usr/bin/start.sh 39 | 40 | ENTRYPOINT [ "bash", "/usr/bin/start.sh" ] 41 | -------------------------------------------------------------------------------- /README-cn.md: -------------------------------------------------------------------------------- 1 | # GraphGate 2 | 3 | <div align="center"> 4 | <!-- CI --> 5 | <img src="https://github.com/async-graphql/graphgate/workflows/CI/badge.svg" /> 6 | <!-- codecov --> 7 | <img src="https://codecov.io/gh/async-graphql/graphgate/branch/master/graph/badge.svg" /> 8 | <a href="https://github.com/rust-secure-code/safety-dance/"> 9 | <img src="https://img.shields.io/badge/unsafe-forbidden-success.svg?style=flat-square" 10 | alt="Unsafe Rust forbidden" /> 11 | </a> 12 | </div> 13 | 14 | GraphGate 是一个用 Rust 语言实现的 [Apollo Federation](https://www.apollographql.com/apollo-federation) 网关。 15 | 16 | ## 快速体验 17 | 18 | 一个由3个服务(accounts, products, reviews)组成的完整GraphQL API。 19 | 20 | ```shell 21 | docker run -p 8000:8000 scott829/graphgate-standalone-demo:latest 22 | ``` 23 | 24 | 打开浏览器[http://localhost:8000](http://localhost:8000) 25 | 26 | ### 执行查询 27 | 28 | ```graphql 29 | { 30 | topProducts { 31 | upc name price reviews { 32 | body 33 | author { 34 | id 35 | username 36 | } 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | ### 执行订阅 43 | 44 | ```graphql 45 | subscription { 46 | users { 47 | id username reviews { 48 | body 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | ## FAQ 55 | 56 | ### Apollo Federation 是做什么的? 57 | 58 | 在微服务架构中数据可能位于不同的位置,把多个服务提供的 API 合并到一起是一件有挑战的事情。 59 | 60 | 为了解决这个问题,你可以使用 Federation 将API的实现划分为多个可组合服务: 61 | 62 | 与其他分布式 GraphQL 结构(例如模式缝合)不同,Federation 使用声明性编程模型,该模型使每个服务仅实现图中负责的部分。 63 | 64 | ### 为什么要用 Rust 实现它? 65 | 66 | Rust是我最喜欢的编程语言,它安全并且快速,非常适合用于开发API网关这样的基础服务。 67 | 68 | ### GraphGate和Apollo Federation的主要区别是什么? 69 | 70 | 我猜GraphGate的性能会好很多(我还没有做基准测试,但很快会加上),并且**支持订阅**。 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphGate 2 | 3 | <div align="center"> 4 | <!-- CI --> 5 | <img src="https://github.com/async-graphql/graphgate/workflows/CI/badge.svg" /> 6 | <!-- codecov --> 7 | <img src="https://codecov.io/gh/async-graphql/graphgate/branch/master/graph/badge.svg" /> 8 | <a href="https://github.com/rust-secure-code/safety-dance/"> 9 | <img src="https://img.shields.io/badge/unsafe-forbidden-success.svg?style=flat-square" 10 | alt="Unsafe Rust forbidden" /> 11 | </a> 12 | </div> 13 | 14 | GraphGate is [Apollo Federation](https://www.apollographql.com/apollo-federation) implemented in Rust. 15 | 16 | ## Quick start 17 | 18 | A GraphQL API composed of 3 services (accounts, products, reviews). 19 | 20 | ```shell 21 | docker run -p 8000:8000 scott829/graphgate-standalone-demo:latest 22 | ``` 23 | 24 | Open browser [http://localhost:8000](http://localhost:8000) 25 | 26 | ### Execute query 27 | 28 | ```graphql 29 | { 30 | topProducts { 31 | upc name price reviews { 32 | body 33 | author { 34 | id 35 | username 36 | } 37 | } 38 | } 39 | } 40 | ``` 41 | 42 | ### Execute subscription 43 | 44 | ```graphql 45 | subscription { 46 | users { 47 | id username reviews { 48 | body 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | ## FAQ 55 | 56 | ### What does Apollo Federation do? 57 | 58 | To get the most out of GraphQL, your organization should expose a single data graph that provides a unified interface for querying any combination of your backing data sources. However, it can be challenging to represent an enterprise-scale data graph with a single, monolithic GraphQL server. 59 | 60 | To remedy this, you can divide your graph's implementation across multiple composable services with Apollo Federation: 61 | 62 | Unlike other distributed GraphQL architectures (such as schema stitching), Apollo Federation uses a declarative programming model that enables each implementing service to implement only the part of your graph that it's responsible for. 63 | 64 | ### Why use Rust to implement it? 65 | 66 | Rust is my favorite programming language. It is safe and fast, and is very suitable for developing API gateway. 67 | 68 | ### What is the difference between GraphGate and Apollo Federation? 69 | 70 | I guess the performance of GraphGate will be much better (I haven't done benchmarking yet, but will add it soon), and it supports subscription. 71 | -------------------------------------------------------------------------------- /benchmark.lua: -------------------------------------------------------------------------------- 1 | wrk.method = "POST" 2 | wrk.headers["Content-Type"] = "application/json" 3 | 4 | local query = "{ topProducts { upc price reviews { body author { id username } } } }" 5 | local body = string.format("{\"operationName\":null,\"variables\":{},\"query\":\"%s\"}", query) 6 | 7 | function request() 8 | return wrk.format('POST', nil, nil, body) 9 | end -------------------------------------------------------------------------------- /benchmark.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | wrk -d 3 -t 32 -c 100 -s benchmark.lua http://127.0.0.1:8000 3 | -------------------------------------------------------------------------------- /crates/handler/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphgate-handler" 3 | version = "0.5.1" 4 | authors = ["Sunli <scott_s829@163.com>"] 5 | edition = "2018" 6 | description = "GraphGate is Apollo Federation implemented in Rust" 7 | license = "MIT/Apache-2.0" 8 | homepage = "https://github.com/async-graphql/graphgate" 9 | repository = "https://github.com/async-graphql/graphgate" 10 | keywords = ["gateway", "graphql", "federation"] 11 | 12 | [dependencies] 13 | graphgate-schema = { version = "0.5.0", path = "../schema" } 14 | graphgate-planner = { version = "0.5.0", path = "../planner" } 15 | 16 | warp = "0.3.2" 17 | indexmap = { version = "1.8.0", features = ["serde-1"] } 18 | futures-util = { version = "0.3.19", features = ["sink"] } 19 | parser = { version = "3.0.24", package = "async-graphql-parser" } 20 | value = { version = "3.0.24", package = "async-graphql-value" } 21 | once_cell = "1.9.0" 22 | tokio = { version = "1.15.0", features = ["net", "sync", "macros", "time"] } 23 | tokio-stream = "0.1.8" 24 | tokio-tungstenite = { version = "0.16.1", features = ["rustls-tls-native-roots"] } 25 | async-stream = "0.3.2" 26 | tracing = "0.1.29" 27 | anyhow = "1.0.52" 28 | http = "0.2.6" 29 | serde = "1.0.133" 30 | serde_json = "1.0.75" 31 | reqwest = { version = "0.11.9", default-features = false, features = ["rustls-tls", "gzip", "brotli", "json"] } 32 | async-trait = "0.1.52" 33 | opentelemetry = { version = "0.16.0", features = ["metrics"] } 34 | chrono = { version = "0.4.19", features = ["serde"] } 35 | -------------------------------------------------------------------------------- /crates/handler/src/constants.rs: -------------------------------------------------------------------------------- 1 | use opentelemetry::Key; 2 | 3 | pub const KEY_SERVICE: Key = Key::from_static_str("graphgate.service"); 4 | pub const KEY_QUERY: Key = Key::from_static_str("graphgate.query"); 5 | pub const KEY_PATH: Key = Key::from_static_str("graphgate.path"); 6 | pub const KEY_PARENT_TYPE: Key = Key::from_static_str("graphgate.parentType"); 7 | pub const KEY_RETURN_TYPE: Key = Key::from_static_str("graphgate.returnType"); 8 | pub const KEY_FIELD_NAME: Key = Key::from_static_str("graphgate.fieldName"); 9 | pub const KEY_VARIABLES: Key = Key::from_static_str("graphgate.variables"); 10 | pub const KEY_ERROR: Key = Key::from_static_str("graphgate.error"); 11 | -------------------------------------------------------------------------------- /crates/handler/src/fetcher.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicU64, Ordering}; 2 | 3 | use anyhow::Result; 4 | use graphgate_planner::{Request, Response}; 5 | use http::HeaderMap; 6 | use tokio::sync::mpsc; 7 | 8 | use crate::websocket::WebSocketController; 9 | use crate::ServiceRouteTable; 10 | 11 | #[async_trait::async_trait] 12 | pub trait Fetcher: Send + Sync { 13 | async fn query(&self, service: &str, request: Request) -> Result<Response>; 14 | } 15 | 16 | pub struct HttpFetcher<'a> { 17 | router_table: &'a ServiceRouteTable, 18 | header_map: &'a HeaderMap, 19 | } 20 | 21 | impl<'a> HttpFetcher<'a> { 22 | pub fn new(router_table: &'a ServiceRouteTable, header_map: &'a HeaderMap) -> Self { 23 | Self { 24 | router_table, 25 | header_map, 26 | } 27 | } 28 | } 29 | 30 | #[async_trait::async_trait] 31 | impl<'a> Fetcher for HttpFetcher<'a> { 32 | async fn query(&self, service: &str, request: Request) -> Result<Response> { 33 | self.router_table 34 | .query(service, request, Some(self.header_map), None) 35 | .await 36 | } 37 | } 38 | 39 | pub struct WebSocketFetcher { 40 | controller: WebSocketController, 41 | id: AtomicU64, 42 | } 43 | 44 | impl WebSocketFetcher { 45 | pub fn new(controller: WebSocketController) -> Self { 46 | Self { 47 | controller, 48 | id: Default::default(), 49 | } 50 | } 51 | } 52 | 53 | #[async_trait::async_trait] 54 | impl Fetcher for WebSocketFetcher { 55 | async fn query(&self, service: &str, request: Request) -> Result<Response> { 56 | let id = self.id.fetch_add(1, Ordering::Relaxed); 57 | let (tx, mut rx) = mpsc::unbounded_channel(); 58 | self.controller 59 | .subscribe(format!("__req{}", id), service, request, tx) 60 | .await?; 61 | rx.recv() 62 | .await 63 | .ok_or_else(|| anyhow::anyhow!("Connection closed.")) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /crates/handler/src/handler.rs: -------------------------------------------------------------------------------- 1 | use std::convert::{Infallible, TryInto}; 2 | use std::net::SocketAddr; 3 | use std::str::FromStr; 4 | use std::sync::Arc; 5 | 6 | use graphgate_planner::Request; 7 | use http::header::HeaderName; 8 | use http::HeaderMap; 9 | use opentelemetry::trace::{FutureExt, TraceContextExt, Tracer}; 10 | use opentelemetry::{global, Context}; 11 | use warp::http::Response as HttpResponse; 12 | use warp::ws::Ws; 13 | use warp::{Filter, Rejection, Reply}; 14 | 15 | use crate::constants::*; 16 | use crate::metrics::METRICS; 17 | use crate::{websocket, SharedRouteTable}; 18 | use std::time::Instant; 19 | 20 | #[derive(Clone)] 21 | pub struct HandlerConfig { 22 | pub shared_route_table: SharedRouteTable, 23 | pub forward_headers: Arc<Vec<String>>, 24 | } 25 | 26 | fn do_forward_headers<T: AsRef<str>>( 27 | forward_headers: &[T], 28 | header_map: &HeaderMap, 29 | remote_addr: Option<SocketAddr>, 30 | ) -> HeaderMap { 31 | let mut new_header_map = HeaderMap::new(); 32 | for name in forward_headers { 33 | for value in header_map.get_all(name.as_ref()) { 34 | if let Ok(name) = HeaderName::from_str(name.as_ref()) { 35 | new_header_map.append(name, value.clone()); 36 | } 37 | } 38 | } 39 | if let Some(remote_addr) = remote_addr { 40 | if let Ok(remote_addr) = remote_addr.to_string().try_into() { 41 | new_header_map.append(warp::http::header::FORWARDED, remote_addr); 42 | } 43 | } 44 | new_header_map 45 | } 46 | 47 | pub fn graphql_request( 48 | config: HandlerConfig, 49 | ) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone { 50 | warp::post() 51 | .and(warp::body::json()) 52 | .and(warp::header::headers_cloned()) 53 | .and(warp::addr::remote()) 54 | .and_then({ 55 | move |request: Request, header_map: HeaderMap, remote_addr: Option<SocketAddr>| { 56 | let config = config.clone(); 57 | async move { 58 | let tracer = global::tracer("graphql"); 59 | 60 | let query = Context::current_with_span( 61 | tracer 62 | .span_builder("query") 63 | .with_attributes(vec![ 64 | KEY_QUERY.string(request.query.clone()), 65 | KEY_VARIABLES 66 | .string(serde_json::to_string(&request.variables).unwrap()), 67 | ]) 68 | .start(&tracer), 69 | ); 70 | 71 | let start_time = Instant::now(); 72 | let resp = config 73 | .shared_route_table 74 | .query( 75 | request, 76 | do_forward_headers(&config.forward_headers, &header_map, remote_addr), 77 | ) 78 | .with_context(query) 79 | .await; 80 | 81 | METRICS 82 | .query_histogram 83 | .record((Instant::now() - start_time).as_secs_f64()); 84 | METRICS.query_counter.add(1); 85 | 86 | Ok::<_, Infallible>(resp) 87 | } 88 | } 89 | }) 90 | } 91 | 92 | pub fn graphql_websocket( 93 | config: HandlerConfig, 94 | ) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone { 95 | warp::ws() 96 | .and(warp::get()) 97 | .and(warp::header::exact_ignore_case("upgrade", "websocket")) 98 | .and(warp::header::optional::<String>("sec-websocket-protocol")) 99 | .and(warp::header::headers_cloned()) 100 | .and(warp::addr::remote()) 101 | .map({ 102 | move |ws: Ws, protocols: Option<String>, header_map, remote_addr: Option<SocketAddr>| { 103 | let config = config.clone(); 104 | let protocol = protocols 105 | .and_then(|protocols| { 106 | protocols 107 | .split(',') 108 | .find_map(|p| websocket::Protocols::from_str(p.trim()).ok()) 109 | }) 110 | .unwrap_or(websocket::Protocols::SubscriptionsTransportWS); 111 | let header_map = 112 | do_forward_headers(&config.forward_headers, &header_map, remote_addr); 113 | 114 | let reply = ws.on_upgrade(move |websocket| async move { 115 | if let Some((composed_schema, route_table)) = 116 | config.shared_route_table.get().await 117 | { 118 | websocket::server( 119 | composed_schema, 120 | route_table, 121 | websocket, 122 | protocol, 123 | header_map, 124 | ) 125 | .await; 126 | } 127 | }); 128 | 129 | warp::reply::with_header( 130 | reply, 131 | "Sec-WebSocket-Protocol", 132 | protocol.sec_websocket_protocol(), 133 | ) 134 | } 135 | }) 136 | } 137 | 138 | pub fn graphql_playground() -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone { 139 | warp::get().map(|| { 140 | HttpResponse::builder() 141 | .header("content-type", "text/html") 142 | .body(include_str!("playground.html")) 143 | }) 144 | } 145 | -------------------------------------------------------------------------------- /crates/handler/src/introspection/enum_value.rs: -------------------------------------------------------------------------------- 1 | use graphgate_planner::IntrospectionSelectionSet; 2 | use graphgate_schema::{ComposedSchema, MetaEnumValue}; 3 | use value::ConstValue; 4 | 5 | use super::resolver::{resolve_obj, Resolver}; 6 | 7 | pub struct IntrospectionEnumValue<'a>(pub &'a MetaEnumValue); 8 | 9 | impl<'a> Resolver for IntrospectionEnumValue<'a> { 10 | fn resolve( 11 | &self, 12 | selection_set: &IntrospectionSelectionSet, 13 | _schema: &ComposedSchema, 14 | ) -> ConstValue { 15 | resolve_obj(selection_set, |name, _field| match name { 16 | "name" => ConstValue::String(self.0.value.to_string()), 17 | "description" => self 18 | .0 19 | .description 20 | .as_ref() 21 | .map(|description| ConstValue::String(description.clone())) 22 | .unwrap_or_default(), 23 | "isDeprecated" => ConstValue::Boolean(self.0.deprecation.is_deprecated()), 24 | "deprecationReason" => self 25 | .0 26 | .deprecation 27 | .reason() 28 | .map(|reason| ConstValue::String(reason.to_string())) 29 | .unwrap_or_default(), 30 | _ => ConstValue::Null, 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /crates/handler/src/introspection/field.rs: -------------------------------------------------------------------------------- 1 | use graphgate_planner::IntrospectionSelectionSet; 2 | use graphgate_schema::{ComposedSchema, MetaField}; 3 | use value::ConstValue; 4 | 5 | use super::input_value::IntrospectionInputValue; 6 | use super::r#type::IntrospectionType; 7 | use super::resolver::{resolve_obj, Resolver}; 8 | 9 | pub struct IntrospectionField<'a>(pub &'a MetaField); 10 | 11 | impl<'a> Resolver for IntrospectionField<'a> { 12 | fn resolve( 13 | &self, 14 | selection_set: &IntrospectionSelectionSet, 15 | schema: &ComposedSchema, 16 | ) -> ConstValue { 17 | resolve_obj(selection_set, |name, field| match name { 18 | "name" => ConstValue::String(self.0.name.to_string()), 19 | "description" => self 20 | .0 21 | .description 22 | .as_ref() 23 | .map(|description| ConstValue::String(description.clone())) 24 | .unwrap_or_default(), 25 | "isDeprecated" => ConstValue::Boolean(self.0.deprecation.is_deprecated()), 26 | "args" => ConstValue::List( 27 | self.0 28 | .arguments 29 | .values() 30 | .map(|arg| IntrospectionInputValue(arg).resolve(&field.selection_set, schema)) 31 | .collect(), 32 | ), 33 | "type" => { 34 | IntrospectionType::new(&self.0.ty, schema).resolve(&field.selection_set, schema) 35 | } 36 | "deprecationReason" => self 37 | .0 38 | .deprecation 39 | .reason() 40 | .map(|reason| ConstValue::String(reason.to_string())) 41 | .unwrap_or_default(), 42 | _ => ConstValue::Null, 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/handler/src/introspection/input_value.rs: -------------------------------------------------------------------------------- 1 | use graphgate_planner::IntrospectionSelectionSet; 2 | use graphgate_schema::{ComposedSchema, MetaInputValue}; 3 | use value::ConstValue; 4 | 5 | use super::r#type::IntrospectionType; 6 | use super::resolver::{resolve_obj, Resolver}; 7 | 8 | pub struct IntrospectionInputValue<'a>(pub &'a MetaInputValue); 9 | 10 | impl<'a> Resolver for IntrospectionInputValue<'a> { 11 | fn resolve( 12 | &self, 13 | selection_set: &IntrospectionSelectionSet, 14 | schema: &ComposedSchema, 15 | ) -> ConstValue { 16 | resolve_obj(selection_set, |name, field| match name { 17 | "name" => ConstValue::String(self.0.name.to_string()), 18 | "description" => self 19 | .0 20 | .description 21 | .as_ref() 22 | .map(|description| ConstValue::String(description.clone())) 23 | .unwrap_or_default(), 24 | "type" => { 25 | IntrospectionType::new(&self.0.ty, schema).resolve(&field.selection_set, schema) 26 | } 27 | "defaultValue" => match &self.0.default_value { 28 | Some(value) => ConstValue::String(value.to_string()), 29 | None => ConstValue::Null, 30 | }, 31 | _ => ConstValue::Null, 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/handler/src/introspection/mod.rs: -------------------------------------------------------------------------------- 1 | mod resolver; 2 | 3 | mod enum_value; 4 | mod field; 5 | mod input_value; 6 | mod root; 7 | mod schema; 8 | mod r#type; 9 | 10 | pub use resolver::Resolver; 11 | pub use root::IntrospectionRoot; 12 | -------------------------------------------------------------------------------- /crates/handler/src/introspection/resolver.rs: -------------------------------------------------------------------------------- 1 | use graphgate_planner::{IntrospectionDirective, IntrospectionField, IntrospectionSelectionSet}; 2 | use graphgate_schema::ComposedSchema; 3 | use indexmap::IndexMap; 4 | use value::{ConstValue, Name}; 5 | 6 | pub trait Resolver { 7 | fn resolve( 8 | &self, 9 | selection_set: &IntrospectionSelectionSet, 10 | schema: &ComposedSchema, 11 | ) -> ConstValue; 12 | } 13 | 14 | pub fn resolve_obj( 15 | selection_set: &IntrospectionSelectionSet, 16 | resolve_fn: impl Fn(&str, &IntrospectionField) -> ConstValue, 17 | ) -> ConstValue { 18 | let mut obj = IndexMap::new(); 19 | for field in &selection_set.0 { 20 | if is_skip(&field.directives) { 21 | continue; 22 | } 23 | let key = field 24 | .alias 25 | .as_ref() 26 | .cloned() 27 | .unwrap_or_else(|| field.name.clone()); 28 | obj.insert(key, resolve_fn(field.name.as_str(), field)); 29 | } 30 | ConstValue::Object(obj) 31 | } 32 | 33 | fn is_skip(directives: &[IntrospectionDirective]) -> bool { 34 | for directive in directives { 35 | let include = match &*directive.name.as_str() { 36 | "skip" => false, 37 | "include" => true, 38 | _ => continue, 39 | }; 40 | 41 | let condition_input = directive.arguments.get("if").unwrap(); 42 | let value = match condition_input { 43 | ConstValue::Boolean(value) => *value, 44 | _ => false, 45 | }; 46 | return include != value; 47 | } 48 | false 49 | } 50 | 51 | pub fn is_include_deprecated(arguments: &IndexMap<Name, ConstValue>) -> bool { 52 | if let Some(ConstValue::Boolean(value)) = arguments.get("includeDeprecated") { 53 | *value 54 | } else { 55 | false 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /crates/handler/src/introspection/root.rs: -------------------------------------------------------------------------------- 1 | use graphgate_planner::IntrospectionSelectionSet; 2 | use graphgate_schema::ComposedSchema; 3 | use value::ConstValue; 4 | 5 | use super::r#type::IntrospectionType; 6 | use super::resolver::{resolve_obj, Resolver}; 7 | use super::schema::IntrospectionSchema; 8 | 9 | pub struct IntrospectionRoot; 10 | 11 | impl Resolver for IntrospectionRoot { 12 | fn resolve( 13 | &self, 14 | selection_set: &IntrospectionSelectionSet, 15 | schema: &ComposedSchema, 16 | ) -> ConstValue { 17 | resolve_obj(selection_set, |name, field| match name { 18 | "__schema" => IntrospectionSchema.resolve(&field.selection_set, schema), 19 | "__type" => { 20 | if let Some(ConstValue::String(name)) = field.arguments.get("name") { 21 | if let Some(ty) = schema.types.get(name.as_str()) { 22 | return IntrospectionType::Named(ty).resolve(&field.selection_set, schema); 23 | } 24 | } 25 | ConstValue::Null 26 | } 27 | _ => ConstValue::Null, 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/handler/src/introspection/schema.rs: -------------------------------------------------------------------------------- 1 | use graphgate_planner::IntrospectionSelectionSet; 2 | use graphgate_schema::ComposedSchema; 3 | use value::ConstValue; 4 | 5 | use super::r#type::IntrospectionType; 6 | use super::resolver::{resolve_obj, Resolver}; 7 | 8 | pub struct IntrospectionSchema; 9 | 10 | impl Resolver for IntrospectionSchema { 11 | fn resolve( 12 | &self, 13 | selection_set: &IntrospectionSelectionSet, 14 | schema: &ComposedSchema, 15 | ) -> ConstValue { 16 | resolve_obj(selection_set, |name, field| match name { 17 | "types" => ConstValue::List( 18 | schema 19 | .types 20 | .values() 21 | .filter(|ty| !ty.name.starts_with("__")) 22 | .map(|ty| IntrospectionType::Named(ty).resolve(&field.selection_set, schema)) 23 | .collect(), 24 | ), 25 | "queryType" => { 26 | let query_type = schema 27 | .types 28 | .get(schema.query_type()) 29 | .expect("The query validator should find this error."); 30 | IntrospectionType::Named(query_type).resolve(&field.selection_set, schema) 31 | } 32 | "mutationType" => { 33 | let mutation_type = schema 34 | .mutation_type 35 | .as_ref() 36 | .and_then(|name| schema.types.get(name)); 37 | match mutation_type { 38 | Some(ty) => IntrospectionType::Named(ty).resolve(&field.selection_set, schema), 39 | None => ConstValue::Null, 40 | } 41 | } 42 | "subscriptionType" => { 43 | let subscription_type = schema 44 | .subscription_type 45 | .as_ref() 46 | .and_then(|name| schema.types.get(name)); 47 | match subscription_type { 48 | Some(ty) => IntrospectionType::Named(ty).resolve(&field.selection_set, schema), 49 | None => ConstValue::Null, 50 | } 51 | } 52 | _ => ConstValue::Null, 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /crates/handler/src/introspection/type.rs: -------------------------------------------------------------------------------- 1 | use graphgate_planner::IntrospectionSelectionSet; 2 | use graphgate_schema::{ComposedSchema, MetaType, TypeKind}; 3 | use once_cell::sync::Lazy; 4 | use parser::types::{BaseType, Type}; 5 | use value::{ConstValue, Name}; 6 | 7 | use super::enum_value::IntrospectionEnumValue; 8 | use super::field::IntrospectionField; 9 | use super::input_value::IntrospectionInputValue; 10 | use super::resolver::{is_include_deprecated, resolve_obj, Resolver}; 11 | 12 | static SCALAR: Lazy<Name> = Lazy::new(|| Name::new("SCALAR")); 13 | static OBJECT: Lazy<Name> = Lazy::new(|| Name::new("OBJECT")); 14 | static INTERFACE: Lazy<Name> = Lazy::new(|| Name::new("INTERFACE")); 15 | static UNION: Lazy<Name> = Lazy::new(|| Name::new("UNION")); 16 | static ENUM: Lazy<Name> = Lazy::new(|| Name::new("ENUM")); 17 | static INPUT_OBJECT: Lazy<Name> = Lazy::new(|| Name::new("INPUT_OBJECT")); 18 | static NON_NULL: Lazy<Name> = Lazy::new(|| Name::new("NON_NULL")); 19 | static LIST: Lazy<Name> = Lazy::new(|| Name::new("LIST")); 20 | 21 | pub enum IntrospectionType<'a> { 22 | Named(&'a MetaType), 23 | NonNull(Box<IntrospectionType<'a>>), 24 | List(Box<IntrospectionType<'a>>), 25 | } 26 | 27 | impl<'a> IntrospectionType<'a> { 28 | pub fn new(ty: &'a Type, schema: &'a ComposedSchema) -> Self { 29 | if ty.nullable { 30 | Self::new_base(&ty.base, schema) 31 | } else { 32 | IntrospectionType::NonNull(Box::new(Self::new_base(&ty.base, schema))) 33 | } 34 | } 35 | 36 | fn new_base(ty: &'a BaseType, schema: &'a ComposedSchema) -> Self { 37 | match ty { 38 | BaseType::Named(name) => IntrospectionType::Named( 39 | schema 40 | .types 41 | .get(name) 42 | .expect("The query validator should find this error."), 43 | ), 44 | BaseType::List(ty) => IntrospectionType::List(Box::new(Self::new(ty, schema))), 45 | } 46 | } 47 | } 48 | 49 | impl<'a> Resolver for IntrospectionType<'a> { 50 | fn resolve( 51 | &self, 52 | selection_set: &IntrospectionSelectionSet, 53 | schema: &ComposedSchema, 54 | ) -> ConstValue { 55 | resolve_obj(selection_set, |name, field| match name { 56 | "kind" => match self { 57 | Self::Named(ty) => match ty.kind { 58 | TypeKind::Scalar => ConstValue::Enum(SCALAR.clone()), 59 | TypeKind::Object => ConstValue::Enum(OBJECT.clone()), 60 | TypeKind::Interface => ConstValue::Enum(INTERFACE.clone()), 61 | TypeKind::Union => ConstValue::Enum(UNION.clone()), 62 | TypeKind::Enum => ConstValue::Enum(ENUM.clone()), 63 | TypeKind::InputObject => ConstValue::Enum(INPUT_OBJECT.clone()), 64 | }, 65 | Self::NonNull(_) => ConstValue::Enum(NON_NULL.clone()), 66 | Self::List(_) => ConstValue::Enum(LIST.clone()), 67 | }, 68 | "name" => match self { 69 | Self::Named(ty) => ConstValue::String(ty.name.to_string()), 70 | _ => ConstValue::Null, 71 | }, 72 | "description" => match self { 73 | Self::Named(ty) => ty 74 | .description 75 | .as_ref() 76 | .map(|description| ConstValue::String(description.clone())) 77 | .unwrap_or_default(), 78 | _ => ConstValue::Null, 79 | }, 80 | "fields" => match self { 81 | Self::Named(ty) 82 | if ty.kind == TypeKind::Object || ty.kind == TypeKind::Interface => 83 | { 84 | ConstValue::List( 85 | ty.fields 86 | .values() 87 | .filter(|item| !item.name.starts_with("__")) 88 | .filter(|item| { 89 | if is_include_deprecated(&field.arguments) { 90 | true 91 | } else { 92 | !item.deprecation.is_deprecated() 93 | } 94 | }) 95 | .map(|f| IntrospectionField(f).resolve(&field.selection_set, schema)) 96 | .collect(), 97 | ) 98 | } 99 | _ => ConstValue::Null, 100 | }, 101 | "interfaces" => match self { 102 | Self::Named(ty) if ty.kind == TypeKind::Object => ConstValue::List( 103 | ty.implements 104 | .iter() 105 | .map(|name| { 106 | IntrospectionType::Named( 107 | schema 108 | .types 109 | .get(name) 110 | .expect("The query validator should find this error."), 111 | ) 112 | .resolve(&field.selection_set, schema) 113 | }) 114 | .collect(), 115 | ), 116 | _ => ConstValue::Null, 117 | }, 118 | "possibleTypes" => match self { 119 | Self::Named(ty) if ty.kind == TypeKind::Interface || ty.kind == TypeKind::Union => { 120 | ConstValue::List( 121 | ty.possible_types 122 | .iter() 123 | .map(|name| { 124 | IntrospectionType::Named( 125 | schema 126 | .types 127 | .get(name) 128 | .expect("The query validator should find this error."), 129 | ) 130 | .resolve(&field.selection_set, schema) 131 | }) 132 | .collect(), 133 | ) 134 | } 135 | _ => ConstValue::Null, 136 | }, 137 | "enumValues" => match self { 138 | Self::Named(ty) if ty.kind == TypeKind::Enum => ConstValue::List( 139 | ty.enum_values 140 | .values() 141 | .filter(|item| { 142 | if is_include_deprecated(&field.arguments) { 143 | true 144 | } else { 145 | !item.deprecation.is_deprecated() 146 | } 147 | }) 148 | .map(|value| { 149 | IntrospectionEnumValue(value).resolve(&field.selection_set, schema) 150 | }) 151 | .collect(), 152 | ), 153 | _ => ConstValue::Null, 154 | }, 155 | "inputFields" => match self { 156 | Self::Named(ty) if ty.kind == TypeKind::InputObject => ConstValue::List( 157 | ty.input_fields 158 | .values() 159 | .map(|value| { 160 | IntrospectionInputValue(value).resolve(&field.selection_set, schema) 161 | }) 162 | .collect(), 163 | ), 164 | _ => ConstValue::Null, 165 | }, 166 | "ofType" => match self { 167 | Self::Named(_) => ConstValue::Null, 168 | Self::List(ty) | Self::NonNull(ty) => ty.resolve(&field.selection_set, schema), 169 | }, 170 | _ => ConstValue::Null, 171 | }) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /crates/handler/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | pub use service_route::{ServiceRoute, ServiceRouteTable}; 4 | pub use shared_route_table::SharedRouteTable; 5 | 6 | mod constants; 7 | mod executor; 8 | mod fetcher; 9 | mod introspection; 10 | mod metrics; 11 | mod service_route; 12 | mod shared_route_table; 13 | mod websocket; 14 | 15 | pub mod handler; 16 | -------------------------------------------------------------------------------- /crates/handler/src/metrics.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use opentelemetry::global; 3 | use opentelemetry::metrics::{BoundCounter, BoundValueRecorder}; 4 | 5 | pub struct Metrics { 6 | pub query_counter: BoundCounter<'static, u64>, 7 | pub query_histogram: BoundValueRecorder<'static, f64>, 8 | } 9 | 10 | pub static METRICS: Lazy<Metrics> = Lazy::new(|| { 11 | let meter = global::meter("graphgate"); 12 | let query_counter = meter 13 | .u64_counter("graphgate.queries_total") 14 | .with_description("Total number of GraphQL queries executed") 15 | .init() 16 | .bind(&[]); 17 | let query_histogram = meter 18 | .f64_value_recorder("graphgate.graphql_query_duration_seconds") 19 | .with_description("The GraphQL query latencies in seconds.") 20 | .init() 21 | .bind(&[]); 22 | Metrics { 23 | query_counter, 24 | query_histogram, 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /crates/handler/src/service_route.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::ops::{Deref, DerefMut}; 3 | 4 | use futures_util::TryFutureExt; 5 | use graphgate_planner::{Request, Response}; 6 | use http::HeaderMap; 7 | use once_cell::sync::Lazy; 8 | 9 | static HTTP_CLIENT: Lazy<reqwest::Client> = Lazy::new(Default::default); 10 | 11 | /// Service routing information. 12 | #[derive(Clone, Eq, PartialEq, Debug)] 13 | pub struct ServiceRoute { 14 | /// Service address 15 | /// 16 | /// For example: 1.2.3.4:8000, example.com:8080 17 | pub addr: String, 18 | 19 | /// Use TLS 20 | pub tls: bool, 21 | 22 | /// GraphQL HTTP path, default is `/`. 23 | pub query_path: Option<String>, 24 | 25 | /// GraphQL WebSocket path, default is `/`. 26 | pub subscribe_path: Option<String>, 27 | 28 | pub introspection_path: Option<String>, 29 | 30 | pub websocket_path: Option<String>, 31 | } 32 | 33 | /// Service routing table 34 | /// 35 | /// The key is the service name. 36 | #[derive(Default, Debug, Clone, Eq, PartialEq)] 37 | pub struct ServiceRouteTable(HashMap<String, ServiceRoute>); 38 | 39 | impl Deref for ServiceRouteTable { 40 | type Target = HashMap<String, ServiceRoute>; 41 | 42 | fn deref(&self) -> &Self::Target { 43 | &self.0 44 | } 45 | } 46 | 47 | impl DerefMut for ServiceRouteTable { 48 | fn deref_mut(&mut self) -> &mut Self::Target { 49 | &mut self.0 50 | } 51 | } 52 | 53 | impl ServiceRouteTable { 54 | /// Call the GraphQL query of the specified service. 55 | pub async fn query( 56 | &self, 57 | service: impl AsRef<str>, 58 | request: Request, 59 | header_map: Option<&HeaderMap>, 60 | introspection: Option<bool>, 61 | ) -> anyhow::Result<Response> { 62 | let service = service.as_ref(); 63 | let route = self.0.get(service).ok_or_else(|| { 64 | anyhow::anyhow!("Service '{}' is not defined in the routing table.", service) 65 | })?; 66 | 67 | let introspection = introspection.unwrap_or(false); 68 | 69 | let scheme = match route.tls { 70 | true => "https", 71 | false => "http", 72 | }; 73 | 74 | let url = if introspection { 75 | match &route.introspection_path { 76 | Some(path) => format!("{}://{}{}", scheme, route.addr, path), 77 | None => format!("{}://{}", scheme, route.addr), 78 | } 79 | } else { 80 | match &route.query_path { 81 | Some(path) => format!("{}://{}{}", scheme, route.addr, path), 82 | None => format!("{}://{}", scheme, route.addr), 83 | } 84 | }; 85 | 86 | let raw_resp = HTTP_CLIENT 87 | .post(&url) 88 | .headers(header_map.cloned().unwrap_or_default()) 89 | .json(&request) 90 | .send() 91 | .and_then(|res| async move { res.error_for_status() }) 92 | .await?; 93 | 94 | let mut headers: HashMap<String, Vec<String>> = HashMap::new(); 95 | 96 | for (key, val) in raw_resp.headers().iter() { 97 | match headers.get_mut(key.as_str()) { 98 | Some(x) => { 99 | x.push(val.to_str().unwrap().to_string()); 100 | } 101 | None => { 102 | headers.insert( 103 | key.as_str().to_string(), 104 | vec![val.to_str().unwrap().to_string()], 105 | ); 106 | } 107 | } 108 | } 109 | 110 | let mut resp = raw_resp.json::<Response>().await?; 111 | resp.headers = Some(headers); 112 | Ok(resp) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /crates/handler/src/shared_route_table.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::{Context, Error, Result}; 4 | use graphgate_planner::{PlanBuilder, Request, Response, ServerError}; 5 | use graphgate_schema::ComposedSchema; 6 | use http::{header::HeaderName, HeaderValue}; 7 | use opentelemetry::trace::{TraceContextExt, Tracer}; 8 | use opentelemetry::{global, Context as OpenTelemetryContext}; 9 | use serde::Deserialize; 10 | use tokio::sync::{mpsc, RwLock}; 11 | use tokio::time::{Duration, Instant}; 12 | use value::ConstValue; 13 | use warp::http::{HeaderMap, Response as HttpResponse, StatusCode}; 14 | 15 | use crate::executor::Executor; 16 | use crate::fetcher::HttpFetcher; 17 | use crate::service_route::ServiceRouteTable; 18 | 19 | enum Command { 20 | Change(ServiceRouteTable), 21 | } 22 | 23 | struct Inner { 24 | schema: Option<Arc<ComposedSchema>>, 25 | route_table: Option<Arc<ServiceRouteTable>>, 26 | } 27 | 28 | #[derive(Clone)] 29 | pub struct SharedRouteTable { 30 | inner: Arc<RwLock<Inner>>, 31 | tx: mpsc::UnboundedSender<Command>, 32 | receive_headers: Vec<String>, 33 | } 34 | 35 | impl Default for SharedRouteTable { 36 | fn default() -> Self { 37 | let (tx, rx) = mpsc::unbounded_channel(); 38 | let shared_route_table = Self { 39 | inner: Arc::new(RwLock::new(Inner { 40 | schema: None, 41 | route_table: None, 42 | })), 43 | tx, 44 | receive_headers: vec![], 45 | }; 46 | tokio::spawn({ 47 | let shared_route_table = shared_route_table.clone(); 48 | async move { shared_route_table.update_loop(rx).await } 49 | }); 50 | shared_route_table 51 | } 52 | } 53 | 54 | impl SharedRouteTable { 55 | async fn update_loop(self, mut rx: mpsc::UnboundedReceiver<Command>) { 56 | let mut update_interval = tokio::time::interval_at( 57 | Instant::now() + Duration::from_secs(3), 58 | Duration::from_secs(30), 59 | ); 60 | 61 | loop { 62 | tokio::select! { 63 | _ = update_interval.tick() => { 64 | if let Err(err) = self.update().await { 65 | tracing::error!(error = %err, "Failed to update schema."); 66 | } 67 | } 68 | command = rx.recv() => { 69 | if let Some(command) = command { 70 | match command { 71 | Command::Change(route_table) => { 72 | let mut inner = self.inner.write().await; 73 | inner.route_table = Some(Arc::new(route_table)); 74 | inner.schema = None; 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | 83 | async fn update(&self) -> Result<()> { 84 | const QUERY_SDL: &str = "{ _service { sdl }}"; 85 | 86 | #[derive(Deserialize)] 87 | struct ResponseQuery { 88 | #[serde(rename = "_service")] 89 | service: ResponseService, 90 | } 91 | 92 | #[derive(Deserialize)] 93 | struct ResponseService { 94 | sdl: String, 95 | } 96 | 97 | let route_table = match self.inner.read().await.route_table.clone() { 98 | Some(route_table) => route_table, 99 | None => return Ok(()), 100 | }; 101 | 102 | let resp = futures_util::future::try_join_all(route_table.keys().map(|service| { 103 | let route_table = route_table.clone(); 104 | async move { 105 | let resp = route_table 106 | .query(service, Request::new(QUERY_SDL), None, Some(true)) 107 | .await 108 | .with_context(|| format!("Failed to fetch SDL from '{}'.", service))?; 109 | let resp: ResponseQuery = 110 | value::from_value(resp.data).context("Failed to parse response.")?; 111 | let document = parser::parse_schema(resp.service.sdl) 112 | .with_context(|| format!("Invalid SDL from '{}'.", service))?; 113 | Ok::<_, Error>((service.to_string(), document)) 114 | } 115 | })) 116 | .await?; 117 | 118 | let schema = ComposedSchema::combine(resp)?; 119 | self.inner.write().await.schema = Some(Arc::new(schema)); 120 | Ok(()) 121 | } 122 | 123 | pub fn set_route_table(&self, route_table: ServiceRouteTable) { 124 | self.tx.send(Command::Change(route_table)).ok(); 125 | } 126 | 127 | pub fn set_receive_headers(&mut self, receive_headers: Vec<String>) { 128 | self.receive_headers = receive_headers; 129 | } 130 | 131 | pub async fn get(&self) -> Option<(Arc<ComposedSchema>, Arc<ServiceRouteTable>)> { 132 | let (composed_schema, route_table) = { 133 | let inner = self.inner.read().await; 134 | (inner.schema.clone(), inner.route_table.clone()) 135 | }; 136 | composed_schema.zip(route_table) 137 | } 138 | 139 | pub async fn query(&self, request: Request, header_map: HeaderMap) -> HttpResponse<String> { 140 | let tracer = global::tracer("graphql"); 141 | 142 | let document = match tracer.in_span("parse", |_| parser::parse_query(&request.query)) { 143 | Ok(document) => document, 144 | Err(err) => { 145 | return HttpResponse::builder() 146 | .status(StatusCode::BAD_REQUEST) 147 | .body(err.to_string()) 148 | .unwrap(); 149 | } 150 | }; 151 | 152 | let (composed_schema, route_table) = match self.get().await { 153 | Some((composed_schema, route_table)) => (composed_schema, route_table), 154 | _ => { 155 | return HttpResponse::builder() 156 | .status(StatusCode::BAD_REQUEST) 157 | .body( 158 | serde_json::to_string(&Response { 159 | data: ConstValue::Null, 160 | errors: vec![ServerError::new("Not ready.")], 161 | extensions: Default::default(), 162 | headers: Default::default(), 163 | }) 164 | .unwrap(), 165 | ) 166 | .unwrap(); 167 | } 168 | }; 169 | 170 | let mut plan_builder = 171 | PlanBuilder::new(&composed_schema, document).variables(request.variables); 172 | if let Some(operation) = request.operation { 173 | plan_builder = plan_builder.operation_name(operation); 174 | } 175 | 176 | let plan = match tracer.in_span("plan", |_| plan_builder.plan()) { 177 | Ok(plan) => plan, 178 | Err(response) => { 179 | return HttpResponse::builder() 180 | .status(StatusCode::OK) 181 | .body(serde_json::to_string(&response).unwrap()) 182 | .unwrap(); 183 | } 184 | }; 185 | 186 | let executor = Executor::new(&composed_schema); 187 | let resp = opentelemetry::trace::FutureExt::with_context( 188 | executor.execute_query(&HttpFetcher::new(&*route_table, &header_map), &plan), 189 | OpenTelemetryContext::current_with_span(tracer.span_builder("execute").start(&tracer)), 190 | ) 191 | .await; 192 | 193 | let mut builder = HttpResponse::builder().status(StatusCode::OK); 194 | 195 | let mut header_map = HeaderMap::new(); 196 | 197 | match resp.headers.clone() { 198 | Some(x) => { 199 | for (k, v) in x 200 | .into_iter() 201 | .filter(|(k, _v)| self.receive_headers.contains(k)) 202 | { 203 | for val in v { 204 | header_map.append( 205 | HeaderName::from_bytes(k.as_bytes()).unwrap(), 206 | HeaderValue::from_str(&val).unwrap(), 207 | ); 208 | } 209 | } 210 | } 211 | _ => {} 212 | } 213 | 214 | match builder.headers_mut() { 215 | Some(x) => x.extend(header_map), 216 | None => {} 217 | } 218 | 219 | builder.body(serde_json::to_string(&resp).unwrap()).unwrap() 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /crates/handler/src/websocket/grouped_stream.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | use std::collections::HashMap; 3 | use std::hash::Hash; 4 | use std::pin::Pin; 5 | use std::task::{Context, Poll}; 6 | 7 | use futures_util::stream::Stream; 8 | use futures_util::task::AtomicWaker; 9 | use futures_util::StreamExt; 10 | 11 | pub struct GroupedStream<K, S> { 12 | streams: HashMap<K, S>, 13 | waker: AtomicWaker, 14 | } 15 | 16 | impl<K, S> Default for GroupedStream<K, S> { 17 | fn default() -> Self { 18 | Self { 19 | streams: Default::default(), 20 | waker: Default::default(), 21 | } 22 | } 23 | } 24 | 25 | impl<K: Eq + Hash + Clone, S> GroupedStream<K, S> { 26 | #[inline] 27 | pub fn insert(&mut self, key: K, stream: S) { 28 | self.waker.wake(); 29 | self.streams.insert(key, stream); 30 | } 31 | 32 | #[inline] 33 | pub fn remove<Q: ?Sized>(&mut self, key: &Q) 34 | where 35 | K: Borrow<Q>, 36 | Q: Hash + Eq, 37 | { 38 | self.streams.remove(key); 39 | } 40 | 41 | #[inline] 42 | pub fn contains_key<Q: ?Sized>(&self, key: &Q) -> bool 43 | where 44 | K: Borrow<Q>, 45 | Q: Hash + Eq, 46 | { 47 | self.streams.contains_key(key) 48 | } 49 | } 50 | 51 | pub enum StreamEvent<K, T> { 52 | Data(K, T), 53 | Complete(K), 54 | } 55 | 56 | impl<K, T, S> Stream for GroupedStream<K, S> 57 | where 58 | K: Eq + Hash + Clone + Unpin, 59 | S: Stream<Item = T> + Unpin, 60 | { 61 | type Item = StreamEvent<K, T>; 62 | 63 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { 64 | self.waker.register(cx.waker()); 65 | 66 | for (key, stream) in self.streams.iter_mut() { 67 | match stream.poll_next_unpin(cx) { 68 | Poll::Ready(Some(value)) => { 69 | return Poll::Ready(Some(StreamEvent::Data(key.clone(), value))) 70 | } 71 | Poll::Ready(None) => { 72 | let key = key.clone(); 73 | self.streams.remove(&key); 74 | return Poll::Ready(Some(StreamEvent::Complete(key))); 75 | } 76 | Poll::Pending => {} 77 | } 78 | } 79 | 80 | Poll::Pending 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /crates/handler/src/websocket/mod.rs: -------------------------------------------------------------------------------- 1 | mod controller; 2 | mod grouped_stream; 3 | mod protocol; 4 | mod server; 5 | 6 | pub use controller::WebSocketController; 7 | pub use protocol::Protocols; 8 | pub use server::server; 9 | -------------------------------------------------------------------------------- /crates/handler/src/websocket/protocol.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use graphgate_planner::{Request, Response}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Copy, Clone, Eq, PartialEq, Debug)] 6 | pub enum Protocols { 7 | /// [subscriptions-transport-ws protocol](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md). 8 | SubscriptionsTransportWS, 9 | /// [graphql-ws protocol](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md). 10 | GraphQLWS, 11 | } 12 | 13 | impl std::str::FromStr for Protocols { 14 | type Err = Error; 15 | 16 | fn from_str(protocol: &str) -> Result<Self, Self::Err> { 17 | if protocol.eq_ignore_ascii_case("graphql-ws") { 18 | Ok(Protocols::SubscriptionsTransportWS) 19 | } else if protocol.eq_ignore_ascii_case("graphql-transport-ws") { 20 | Ok(Protocols::GraphQLWS) 21 | } else { 22 | anyhow::bail!("Unsupported Sec-WebSocket-Protocol: {}", protocol) 23 | } 24 | } 25 | } 26 | 27 | impl Protocols { 28 | pub fn sec_websocket_protocol(&self) -> &str { 29 | match self { 30 | Protocols::SubscriptionsTransportWS => "graphql-ws", 31 | Protocols::GraphQLWS => "graphql-transport-ws", 32 | } 33 | } 34 | 35 | #[inline] 36 | pub fn subscribe_message<'a>(&self, id: &'a str, request: Request) -> ClientMessage<'a> { 37 | match self { 38 | Protocols::SubscriptionsTransportWS => ClientMessage::Start { 39 | id, 40 | payload: request, 41 | }, 42 | Protocols::GraphQLWS => ClientMessage::Subscribe { 43 | id, 44 | payload: request, 45 | }, 46 | } 47 | } 48 | 49 | #[inline] 50 | pub fn next_message<'a>(&self, id: &'a str, payload: Response) -> ServerMessage<'a> { 51 | match self { 52 | Protocols::SubscriptionsTransportWS => ServerMessage::Data { id, payload }, 53 | Protocols::GraphQLWS => ServerMessage::Next { id, payload }, 54 | } 55 | } 56 | } 57 | 58 | #[derive(Serialize, Deserialize)] 59 | #[serde(tag = "type", rename_all = "snake_case")] 60 | #[allow(dead_code)] 61 | pub enum ClientMessage<'a> { 62 | ConnectionInit { payload: Option<serde_json::Value> }, 63 | Start { id: &'a str, payload: Request }, 64 | Subscribe { id: &'a str, payload: Request }, 65 | Stop { id: &'a str }, 66 | Complete { id: &'a str }, 67 | ConnectionTerminate, 68 | } 69 | 70 | #[derive(Deserialize, Serialize)] 71 | pub struct ConnectionError<'a> { 72 | pub message: &'a str, 73 | } 74 | 75 | #[derive(Deserialize, Serialize)] 76 | #[serde(tag = "type", rename_all = "snake_case")] 77 | #[allow(dead_code)] 78 | pub enum ServerMessage<'a> { 79 | ConnectionError { payload: ConnectionError<'a> }, 80 | ConnectionAck, 81 | Data { id: &'a str, payload: Response }, 82 | Next { id: &'a str, payload: Response }, 83 | Complete { id: &'a str }, 84 | } 85 | -------------------------------------------------------------------------------- /crates/handler/src/websocket/server.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use futures_util::sink::Sink; 4 | use futures_util::stream::Stream; 5 | use futures_util::{SinkExt, StreamExt}; 6 | use graphgate_planner::{PlanBuilder, Response, ServerError}; 7 | use graphgate_schema::ComposedSchema; 8 | use value::ConstValue; 9 | use warp::http::HeaderMap; 10 | use warp::ws::Message; 11 | use warp::Error; 12 | 13 | use super::controller::WebSocketController; 14 | use super::grouped_stream::{GroupedStream, StreamEvent}; 15 | use super::protocol::{ClientMessage, ConnectionError, Protocols, ServerMessage}; 16 | use crate::executor::Executor; 17 | use crate::ServiceRouteTable; 18 | 19 | pub async fn server( 20 | schema: Arc<ComposedSchema>, 21 | route_table: Arc<ServiceRouteTable>, 22 | stream: impl Stream<Item = Result<Message, Error>> + Sink<Message>, 23 | protocol: Protocols, 24 | header_map: HeaderMap, 25 | ) { 26 | let (mut sink, mut stream) = stream.split(); 27 | let mut streams = GroupedStream::default(); 28 | let mut controller = None; 29 | let header_map = Arc::new(header_map); 30 | 31 | loop { 32 | tokio::select! { 33 | message = stream.next() => match message { 34 | Some(Ok(message)) if message.is_text() => { 35 | let text = message.into_bytes(); 36 | let client_msg = match serde_json::from_slice::<ClientMessage>(&text) { 37 | Ok(client_msg) => client_msg, 38 | Err(_) => return, 39 | }; 40 | 41 | match client_msg { 42 | ClientMessage::ConnectionInit { payload } if controller.is_none() => { 43 | controller = Some(WebSocketController::new(route_table.clone(), &header_map, payload)); 44 | sink.send(Message::text(serde_json::to_string(&ServerMessage::ConnectionAck).unwrap())).await.ok(); 45 | } 46 | ClientMessage::ConnectionInit { .. } => { 47 | match protocol { 48 | Protocols::SubscriptionsTransportWS => { 49 | let err_msg = Message::text( 50 | serde_json::to_string(&ServerMessage::ConnectionError { 51 | payload: ConnectionError { 52 | message: "Too many initialisation requests.", 53 | }, 54 | }).unwrap()); 55 | sink.send(err_msg).await.ok(); 56 | return; 57 | } 58 | Protocols::GraphQLWS => { 59 | sink.send(Message::close_with(4429u16, "Too many initialisation requests.")).await.ok(); 60 | return; 61 | } 62 | } 63 | } 64 | ClientMessage::Start { id, payload } | ClientMessage::Subscribe { id, payload } => { 65 | let controller = controller.get_or_insert_with(|| WebSocketController::new(route_table.clone(), &header_map, None)).clone(); 66 | let document = match parser::parse_query(&payload.query) { 67 | Ok(document) => document, 68 | Err(err) => { 69 | let resp = Response { 70 | data: ConstValue::Null, 71 | errors: vec![ServerError::new(err.to_string())], 72 | extensions: Default::default(), 73 | headers: Default::default() 74 | }; 75 | let data = ServerMessage::Data { id, payload: resp }; 76 | sink.send(Message::text(serde_json::to_string(&data).unwrap())).await.ok(); 77 | 78 | let complete = ServerMessage::Complete { id }; 79 | sink.send(Message::text(serde_json::to_string(&complete).unwrap())).await.ok(); 80 | continue; 81 | } 82 | }; 83 | 84 | let id = Arc::new(id.to_string()); 85 | let schema = schema.clone(); 86 | let stream = { 87 | let id = id.clone(); 88 | async_stream::stream! { 89 | let builder = PlanBuilder::new(&schema, document).variables(payload.variables); 90 | let node = match builder.plan() { 91 | Ok(node) => node, 92 | Err(resp) => { 93 | yield resp; 94 | return; 95 | } 96 | }; 97 | let executor = Executor::new(&schema); 98 | let mut stream = executor.execute_stream(controller.clone(), &id, &node).await; 99 | while let Some(item) = stream.next().await { 100 | yield item; 101 | } 102 | } 103 | }; 104 | streams.insert(id, Box::pin(stream)); 105 | } 106 | ClientMessage::Stop { id } => { 107 | let controller = controller.get_or_insert_with(|| WebSocketController::new(route_table.clone(), &header_map, None)).clone(); 108 | controller.stop(id).await; 109 | } 110 | _ => {} 111 | } 112 | } 113 | Some(Ok(message)) if message.is_close() => return, 114 | Some(Err(_)) | None => return, 115 | _ => {} 116 | }, 117 | item = streams.next() => if let Some(event) = item { 118 | match event { 119 | StreamEvent::Data(id, resp) => { 120 | let data = protocol.next_message(&id, resp); 121 | if sink.send(Message::text(serde_json::to_string(&data).unwrap())).await.is_err() { 122 | return; 123 | } 124 | } 125 | StreamEvent::Complete(id) => { 126 | let complete = ServerMessage::Complete { id: &id }; 127 | if sink.send(Message::text(serde_json::to_string(&complete).unwrap())).await.is_err() { 128 | return; 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /crates/planner/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphgate-planner" 3 | version = "0.5.1" 4 | authors = ["Sunli <scott_s829@163.com>"] 5 | edition = "2018" 6 | description = "GraphGate is Apollo Federation implemented in Rust" 7 | license = "MIT/Apache-2.0" 8 | homepage = "https://github.com/async-graphql/graphgate" 9 | repository = "https://github.com/async-graphql/graphgate" 10 | keywords = ["gateway", "graphql", "federation"] 11 | 12 | [dependencies] 13 | graphgate-schema = { version = "0.5.0", path = "../schema" } 14 | graphgate-validation = { version = "0.5.0", path = "../validation" } 15 | 16 | parser = { version = "3.0.24", package = "async-graphql-parser" } 17 | value = { version = "3.0.24", package = "async-graphql-value" } 18 | indexmap = { version = "1.8.0", features = ["serde-1"] } 19 | serde = "1.0.133" 20 | 21 | [dev-dependencies] 22 | globset = "0.4.8" 23 | serde_json = "1.0.75" 24 | -------------------------------------------------------------------------------- /crates/planner/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | mod builder; 4 | mod plan; 5 | mod request; 6 | mod response; 7 | mod types; 8 | 9 | pub use builder::PlanBuilder; 10 | pub use plan::{ 11 | FetchNode, FlattenNode, IntrospectionDirective, IntrospectionField, IntrospectionNode, 12 | IntrospectionSelectionSet, ParallelNode, PathSegment, PlanNode, ResponsePath, RootNode, 13 | SequenceNode, SubscribeNode, 14 | }; 15 | pub use request::Request; 16 | pub use response::{ErrorPath, Response, ServerError}; 17 | -------------------------------------------------------------------------------- /crates/planner/src/plan.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; 2 | use std::ops::{Deref, DerefMut}; 3 | 4 | use indexmap::IndexMap; 5 | use serde::{Serialize, Serializer}; 6 | use value::{ConstValue, Name, Variables}; 7 | 8 | use crate::types::{FetchQuery, VariablesRef}; 9 | use crate::Request; 10 | 11 | #[derive(Debug, Serialize)] 12 | #[serde(tag = "type", rename_all = "lowercase")] 13 | pub enum PlanNode<'a> { 14 | Sequence(SequenceNode<'a>), 15 | Parallel(ParallelNode<'a>), 16 | Introspection(IntrospectionNode), 17 | Fetch(FetchNode<'a>), 18 | Flatten(FlattenNode<'a>), 19 | } 20 | 21 | impl<'a> PlanNode<'a> { 22 | pub(crate) fn flatten(self) -> Self { 23 | match self { 24 | PlanNode::Sequence(mut node) if node.nodes.len() == 1 => node.nodes.remove(0), 25 | PlanNode::Parallel(mut node) if node.nodes.len() == 1 => node.nodes.remove(0), 26 | _ => self, 27 | } 28 | } 29 | } 30 | 31 | #[derive(Debug, Clone, Hash, Eq, PartialEq)] 32 | pub struct PathSegment<'a> { 33 | pub name: &'a str, 34 | pub is_list: bool, 35 | pub possible_type: Option<&'a str>, 36 | } 37 | 38 | #[derive(Clone, Default, Hash, Eq, PartialEq)] 39 | pub struct ResponsePath<'a>(Vec<PathSegment<'a>>); 40 | 41 | impl<'a> Debug for ResponsePath<'a> { 42 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 43 | for (idx, segment) in self.0.iter().enumerate() { 44 | if idx > 0 { 45 | write!(f, ".")?; 46 | } 47 | if segment.is_list { 48 | write!(f, "[{}]", segment.name)?; 49 | } else { 50 | write!(f, "{}", segment.name)?; 51 | } 52 | if let Some(possible_type) = segment.possible_type { 53 | write!(f, "({})", possible_type)?; 54 | } 55 | } 56 | Ok(()) 57 | } 58 | } 59 | 60 | impl<'a> Display for ResponsePath<'a> { 61 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 62 | Debug::fmt(self, f) 63 | } 64 | } 65 | 66 | impl<'a> Serialize for ResponsePath<'a> { 67 | fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 68 | where 69 | S: Serializer, 70 | { 71 | serializer.serialize_str(&self.to_string()) 72 | } 73 | } 74 | 75 | impl<'a> Deref for ResponsePath<'a> { 76 | type Target = Vec<PathSegment<'a>>; 77 | 78 | fn deref(&self) -> &Self::Target { 79 | &self.0 80 | } 81 | } 82 | 83 | impl<'a> DerefMut for ResponsePath<'a> { 84 | fn deref_mut(&mut self) -> &mut Self::Target { 85 | &mut self.0 86 | } 87 | } 88 | 89 | #[derive(Default, Debug, Serialize)] 90 | pub struct SequenceNode<'a> { 91 | pub nodes: Vec<PlanNode<'a>>, 92 | } 93 | 94 | #[derive(Default, Debug, Serialize)] 95 | pub struct ParallelNode<'a> { 96 | pub nodes: Vec<PlanNode<'a>>, 97 | } 98 | 99 | #[derive(Debug, Serialize)] 100 | pub struct IntrospectionDirective { 101 | pub name: Name, 102 | 103 | #[serde(skip_serializing_if = "IndexMap::is_empty")] 104 | pub arguments: IndexMap<Name, ConstValue>, 105 | } 106 | 107 | #[derive(Debug, Serialize)] 108 | pub struct IntrospectionField { 109 | pub name: Name, 110 | 111 | #[serde(skip_serializing_if = "Option::is_none")] 112 | pub alias: Option<Name>, 113 | 114 | #[serde(skip_serializing_if = "IndexMap::is_empty")] 115 | pub arguments: IndexMap<Name, ConstValue>, 116 | 117 | #[serde(skip_serializing_if = "Vec::is_empty")] 118 | pub directives: Vec<IntrospectionDirective>, 119 | 120 | pub selection_set: IntrospectionSelectionSet, 121 | } 122 | 123 | #[derive(Debug, Default, Serialize)] 124 | #[serde(transparent)] 125 | pub struct IntrospectionSelectionSet(pub Vec<IntrospectionField>); 126 | 127 | #[derive(Debug, Serialize)] 128 | #[serde(transparent)] 129 | pub struct IntrospectionNode { 130 | pub selection_set: IntrospectionSelectionSet, 131 | } 132 | 133 | #[derive(Debug, Serialize)] 134 | #[serde(rename_all = "camelCase")] 135 | pub struct FetchNode<'a> { 136 | pub service: &'a str, 137 | #[serde(skip_serializing_if = "VariablesRef::is_empty")] 138 | pub variables: VariablesRef<'a>, 139 | pub query: FetchQuery<'a>, 140 | } 141 | 142 | impl<'a> FetchNode<'a> { 143 | pub fn to_request(&self) -> Request { 144 | Request::new(self.query.to_string()).variables(self.variables.to_variables()) 145 | } 146 | } 147 | 148 | #[derive(Debug, Serialize)] 149 | #[serde(rename_all = "camelCase")] 150 | pub struct FlattenNode<'a> { 151 | pub path: ResponsePath<'a>, 152 | pub prefix: usize, 153 | pub service: &'a str, 154 | #[serde(skip_serializing_if = "VariablesRef::is_empty")] 155 | pub variables: VariablesRef<'a>, 156 | pub query: FetchQuery<'a>, 157 | } 158 | 159 | impl<'a> FlattenNode<'a> { 160 | pub fn to_request(&self, representations: Variables) -> Request { 161 | Request::new(self.query.to_string()) 162 | .variables(representations) 163 | .extend_variables(self.variables.to_variables()) 164 | } 165 | } 166 | 167 | #[derive(Debug, Serialize)] 168 | #[serde(rename_all = "camelCase")] 169 | pub struct SubscribeNode<'a> { 170 | pub subscribe_nodes: Vec<FetchNode<'a>>, 171 | #[serde(skip_serializing_if = "Option::is_none")] 172 | pub flatten_node: Option<PlanNode<'a>>, 173 | } 174 | 175 | #[derive(Debug, Serialize)] 176 | #[serde(tag = "type", rename_all = "lowercase")] 177 | pub enum RootNode<'a> { 178 | Subscribe(SubscribeNode<'a>), 179 | Query(PlanNode<'a>), 180 | } 181 | -------------------------------------------------------------------------------- /crates/planner/src/request.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use value::{ConstValue, Variables}; 3 | 4 | #[derive(Debug, Serialize, Deserialize)] 5 | pub struct Request { 6 | pub query: String, 7 | pub operation: Option<String>, 8 | #[serde(skip_serializing_if = "variables_is_empty", default)] 9 | pub variables: Variables, 10 | } 11 | 12 | impl Request { 13 | pub fn new(query: impl Into<String>) -> Self { 14 | Self { 15 | query: query.into(), 16 | operation: None, 17 | variables: Default::default(), 18 | } 19 | } 20 | 21 | pub fn operation(self, operation: impl Into<String>) -> Self { 22 | Self { 23 | operation: Some(operation.into()), 24 | ..self 25 | } 26 | } 27 | 28 | pub fn variables(self, variables: Variables) -> Self { 29 | Self { variables, ..self } 30 | } 31 | 32 | pub fn extend_variables(mut self, variables: Variables) -> Self { 33 | if let ConstValue::Object(obj) = variables.into_value() { 34 | self.variables.extend(obj); 35 | } 36 | self 37 | } 38 | } 39 | 40 | #[inline] 41 | fn variables_is_empty(variables: &Variables) -> bool { 42 | variables.is_empty() 43 | } 44 | -------------------------------------------------------------------------------- /crates/planner/src/response.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use parser::Pos; 4 | use serde::{Deserialize, Serialize}; 5 | use value::ConstValue; 6 | 7 | #[derive(Debug, Serialize, Deserialize)] 8 | #[serde(untagged)] 9 | pub enum ErrorPath { 10 | Name(String), 11 | Index(usize), 12 | } 13 | 14 | #[derive(Debug, Serialize, Deserialize)] 15 | pub struct ServerError { 16 | pub message: String, 17 | 18 | #[serde(skip_serializing_if = "Vec::is_empty", default)] 19 | pub path: Vec<ConstValue>, 20 | 21 | #[serde(skip_serializing_if = "Vec::is_empty", default)] 22 | pub locations: Vec<Pos>, 23 | 24 | #[serde(skip_serializing_if = "HashMap::is_empty", default)] 25 | pub extensions: HashMap<String, ConstValue>, 26 | } 27 | 28 | impl ServerError { 29 | pub fn new(message: impl Into<String>) -> Self { 30 | Self { 31 | message: message.into(), 32 | path: Default::default(), 33 | locations: Default::default(), 34 | extensions: Default::default(), 35 | } 36 | } 37 | } 38 | 39 | #[derive(Debug, Serialize, Deserialize, Default)] 40 | pub struct Response { 41 | pub data: ConstValue, 42 | 43 | #[serde(skip_serializing_if = "Vec::is_empty", default)] 44 | pub errors: Vec<ServerError>, 45 | 46 | #[serde(skip_serializing_if = "HashMap::is_empty", default)] 47 | pub extensions: HashMap<String, ConstValue>, 48 | 49 | #[serde(skip_serializing)] 50 | pub headers: Option<HashMap<String, Vec<String>>>, 51 | } 52 | -------------------------------------------------------------------------------- /crates/planner/tests/fragment_on_interface.txt: -------------------------------------------------------------------------------- 1 | fragment AccountDetails on StoreAccount { 2 | __typename 3 | ... on PersonalAccount { 4 | deliveryName 5 | dob 6 | } 7 | ... on BusinessAccount { 8 | taxNumber 9 | businessSector 10 | } 11 | } 12 | 13 | { 14 | me { 15 | id 16 | username 17 | storeAccount { 18 | id 19 | createdAt 20 | ...AccountDetails 21 | } 22 | } 23 | } 24 | --- 25 | {} 26 | --- 27 | { 28 | "type": "fetch", 29 | "service": "accounts", 30 | "query": "query\n{ me { id username storeAccount { ... on PersonalAccount { id createdAt __typename deliveryName dob } ... on BusinessAccount { id createdAt __typename taxNumber businessSector } } } }" 31 | } 32 | -------------------------------------------------------------------------------- /crates/planner/tests/fragment_spread.txt: -------------------------------------------------------------------------------- 1 | fragment A on User { 2 | id username 3 | } 4 | 5 | { 6 | me { 7 | ... A 8 | } 9 | } 10 | --- 11 | {} 12 | --- 13 | { 14 | "type": "fetch", 15 | "service": "accounts", 16 | "query": "query\n{ me { id username } }" 17 | } 18 | -------------------------------------------------------------------------------- /crates/planner/tests/inline_fragment.txt: -------------------------------------------------------------------------------- 1 | { 2 | me { 3 | ... { id username } 4 | } 5 | } 6 | --- 7 | {} 8 | --- 9 | { 10 | "type": "fetch", 11 | "service": "accounts", 12 | "query": "query\n{ me { id username } }" 13 | } 14 | -------------------------------------------------------------------------------- /crates/planner/tests/mutation.txt: -------------------------------------------------------------------------------- 1 | mutation { 2 | u1: createUser(username: "u1") { 3 | id 4 | username 5 | } 6 | u2: createUser(username: "u2") { 7 | id 8 | username 9 | } 10 | review1: createReview(body: "hehe") { 11 | body 12 | } 13 | review2: createReview(body: "haha") { 14 | body 15 | } 16 | u3: createUser(username: "u3") { 17 | id 18 | username 19 | } 20 | } 21 | --- 22 | {} 23 | --- 24 | { 25 | "type": "sequence", 26 | "nodes": [ 27 | { 28 | "type": "fetch", 29 | "service": "accounts", 30 | "query": "mutation\n{ u1:createUser(username: \"u1\") { id username } u2:createUser(username: \"u2\") { id username } }" 31 | }, 32 | { 33 | "type": "fetch", 34 | "service": "reviews", 35 | "query": "mutation\n{ review1:createReview(body: \"hehe\") { body } review2:createReview(body: \"haha\") { body } }" 36 | }, 37 | { 38 | "type": "fetch", 39 | "service": "accounts", 40 | "query": "mutation\n{ u3:createUser(username: \"u3\") { id username } }" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /crates/planner/tests/node_fragments.txt: -------------------------------------------------------------------------------- 1 | fragment BusinessAccountFragment on BusinessAccount { 2 | taxNumber 3 | businessSector 4 | } 5 | 6 | fragment PersonalAccountFragment on PersonalAccount { 7 | dob 8 | deliveryName 9 | } 10 | 11 | 12 | query($nodeId: ID!){ 13 | node(id: $nodeId) { 14 | id 15 | __typename 16 | ... BusinessAccountFragment 17 | ... PersonalAccountFragment 18 | 19 | } 20 | } 21 | --- 22 | { 23 | "nodeId": "6be94a2d-34d0-45fb-927e-42abd3552007" 24 | } 25 | --- 26 | { 27 | "type": "fetch", 28 | "service": "accounts", 29 | "variables": { 30 | "nodeId": "6be94a2d-34d0-45fb-927e-42abd3552007" 31 | }, 32 | "query": "query($nodeId: ID!)\n{ node(id: $nodeId) { ... on PersonalAccount { id __typename dob deliveryName } ... on BusinessAccount { id __typename taxNumber businessSector } } }" 33 | } 34 | -------------------------------------------------------------------------------- /crates/planner/tests/possible_interface.txt: -------------------------------------------------------------------------------- 1 | { 2 | topProducts { 3 | upc 4 | name 5 | price 6 | } 7 | } 8 | --- 9 | {} 10 | --- 11 | { 12 | "type": "sequence", 13 | "nodes": [ 14 | { 15 | "type": "fetch", 16 | "service": "products", 17 | "query": "query\n{ topProducts { ... on Mouse { upc name price } ... on Book { upc __key1___typename:__typename __key1_upc:upc } ... on Car { upc __key2___typename:__typename __key2_upc:upc } } }" 18 | }, 19 | { 20 | "type": "parallel", 21 | "nodes": [ 22 | { 23 | "type": "flatten", 24 | "path": "[topProducts](Book)", 25 | "prefix": 1, 26 | "service": "books", 27 | "query": "query($representations:[_Any!]!) { _entities(representations:$representations) { ... on Book { name price } } }" 28 | }, 29 | { 30 | "type": "flatten", 31 | "path": "[topProducts](Car)", 32 | "prefix": 2, 33 | "service": "cars", 34 | "query": "query($representations:[_Any!]!) { _entities(representations:$representations) { ... on Car { name price } } }" 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | --- 41 | { 42 | topProducts { 43 | upc 44 | name 45 | price 46 | ... on Mouse { 47 | isWireless 48 | } 49 | ... on Book { 50 | isbn issuer publishDate 51 | } 52 | ... on Car { 53 | brand power torque 54 | } 55 | } 56 | } 57 | --- 58 | {} 59 | --- 60 | { 61 | "type": "sequence", 62 | "nodes": [ 63 | { 64 | "type": "fetch", 65 | "service": "products", 66 | "query": "query\n{ topProducts { ... on Mouse { upc name price isWireless } ... on Book { upc __key1___typename:__typename __key1_upc:upc } ... on Car { upc __key2___typename:__typename __key2_upc:upc } } }" 67 | }, 68 | { 69 | "type": "parallel", 70 | "nodes": [ 71 | { 72 | "type": "flatten", 73 | "service": "books", 74 | "prefix": 1, 75 | "path": "[topProducts](Book)", 76 | "query": "query($representations:[_Any!]!) { _entities(representations:$representations) { ... on Book { name price isbn issuer publishDate } } }" 77 | }, 78 | { 79 | "type": "flatten", 80 | "service": "cars", 81 | "prefix": 2, 82 | "path": "[topProducts](Car)", 83 | "query": "query($representations:[_Any!]!) { _entities(representations:$representations) { ... on Car { name price brand power torque } } }" 84 | } 85 | ] 86 | } 87 | ] 88 | } 89 | -------------------------------------------------------------------------------- /crates/planner/tests/possible_union.txt: -------------------------------------------------------------------------------- 1 | { 2 | me { 3 | reviews { 4 | body 5 | attachment { 6 | __typename 7 | ... on Image { 8 | width 9 | height 10 | data 11 | } 12 | ... on Audio { 13 | duration 14 | data 15 | } 16 | ... on Text { 17 | content 18 | } 19 | } 20 | } 21 | } 22 | } 23 | --- 24 | {} 25 | --- 26 | { 27 | "type": "sequence", 28 | "nodes": [ 29 | { 30 | "type": "fetch", 31 | "service": "accounts", 32 | "query": "query\n{ me { __key1___typename:__typename __key1_id:id } }" 33 | }, 34 | { 35 | "type": "flatten", 36 | "service": "reviews", 37 | "path": "me", 38 | "prefix": 1, 39 | "query": "query($representations:[_Any!]!) { _entities(representations:$representations) { ... on User { reviews { body attachment { ... on Text { __typename content } ... on Image { __typename __key2___typename:__typename __key2_id:id } ... on Audio { __typename __key3___typename:__typename __key3_id:id } } } } } }" 40 | }, 41 | { 42 | "type": "parallel", 43 | "nodes": [ 44 | { 45 | "type": "flatten", 46 | "service": "attachments", 47 | "path": "me.[reviews].attachment(Image)", 48 | "prefix": 2, 49 | "query": "query($representations:[_Any!]!) { _entities(representations:$representations) { ... on Image { width height data } } }" 50 | }, 51 | { 52 | "type": "flatten", 53 | "service": "attachments", 54 | "path": "me.[reviews].attachment(Audio)", 55 | "prefix": 3, 56 | "query": "query($representations:[_Any!]!) { _entities(representations:$representations) { ... on Audio { duration data } } }" 57 | } 58 | ] 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /crates/planner/tests/query.txt: -------------------------------------------------------------------------------- 1 | { 2 | u1: user(id: "1234") { 3 | id username 4 | } 5 | me { 6 | id username 7 | } 8 | u2: user(id: "1234") { 9 | id username 10 | } 11 | myName 12 | theirName(id: 42) 13 | } 14 | --- 15 | {} 16 | --- 17 | { 18 | "type": "fetch", 19 | "service": "accounts", 20 | "query": "query\n{ u1:user(id: \"1234\") { id username } me { id username } u2:user(id: \"1234\") { id username } myName theirName(id: 42) }" 21 | } 22 | -------------------------------------------------------------------------------- /crates/planner/tests/subscribe.txt: -------------------------------------------------------------------------------- 1 | subscription { 2 | users { 3 | id username reviews { 4 | body 5 | } 6 | } 7 | } 8 | --- 9 | {} 10 | --- 11 | { 12 | "type": "subscribe", 13 | "subscribeNodes": [ 14 | { 15 | "service": "accounts", 16 | "query": "subscription\n{ users { id username __key1___typename:__typename __key1_id:id } }" 17 | } 18 | ], 19 | "flattenNode": { 20 | "type": "flatten", 21 | "service": "reviews", 22 | "prefix": 1, 23 | "path": "users", 24 | "query": "query($representations:[_Any!]!) { _entities(representations:$representations) { ... on User { reviews { body } } } }" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/planner/tests/test.graphql: -------------------------------------------------------------------------------- 1 | directive @composedGraph(version: Int!) on SCHEMA 2 | directive @owner(service: String!) on OBJECT 3 | directive @key(fields: String! service: String!) on OBJECT 4 | directive @resolve(service: String!) on FIELD_DEFINITION 5 | directive @provides(fields: String!) on FIELD_DEFINITION 6 | directive @requires(fields: String!) on FIELD_DEFINITION 7 | 8 | scalar DateTime 9 | 10 | schema 11 | @composedGraph(version: 1) 12 | { 13 | query: Query 14 | mutation: Mutation 15 | subscription: Subscription 16 | } 17 | 18 | scalar CustomUserID 19 | 20 | type Query { 21 | myName: String! @resolve(service: "accounts") 22 | theirName(id: CustomUserID): String @resolve(service: "accounts") 23 | me: User @resolve(service: "accounts") 24 | user(id: ID!): User @resolve(service: "accounts") 25 | topProducts: [Product!]! @resolve(service: "products") 26 | node(id: ID!): Node @resolve(service: "accounts") 27 | } 28 | 29 | type Mutation { 30 | createUser(username: String): User! @resolve(service: "accounts") 31 | createProduct(name: String!, price: Int!): Product! @resolve(service: "products") 32 | createReview(body: String!, attachmentId: ID): Review! @resolve(service: "reviews") 33 | } 34 | 35 | type Subscription { 36 | users: User @resolve(service: "accounts") 37 | products: Product @resolve(service: "products") 38 | reviews: Review @resolve(service: "reviews") 39 | } 40 | 41 | type User 42 | @owner(service: "accounts") 43 | @key(fields: "id" service: "accounts") 44 | @key(fields: "id" service: "products") 45 | @key(fields: "id" service: "reviews") 46 | { 47 | id: ID! 48 | username: String! 49 | reviews: [Review]! @resolve(service: "reviews") 50 | products: [Product]! @resolve(service: "products") 51 | storeAccount: StoreAccount! 52 | } 53 | 54 | interface Node { 55 | id: ID! 56 | } 57 | 58 | 59 | interface Product { 60 | upc: String! 61 | name: String! 62 | price: Int! 63 | reviews: [Review]! @resolve(service: "reviews") 64 | } 65 | 66 | interface StoreAccount { 67 | createdAt: DateTime! 68 | id: ID! 69 | } 70 | 71 | type PersonalAccount implements StoreAccount & Node 72 | @owner(service: "accounts") 73 | { 74 | createdAt: DateTime! 75 | id: ID! 76 | deliveryName: String! 77 | dob: DateTime! 78 | } 79 | 80 | type BusinessAccount implements StoreAccount & Node 81 | @owner(service: "accounts") 82 | { 83 | createdAt: DateTime! 84 | id: ID! 85 | businessSector: String! 86 | taxNumber: Int! 87 | businessName: String! 88 | } 89 | 90 | type Mouse implements Product 91 | @owner(service: "products") 92 | { 93 | upc: String! 94 | name: String! 95 | price: Int! 96 | reviews: [Review]! @resolve(service: "reviews") 97 | 98 | isWireless: Boolean! 99 | } 100 | 101 | type Book implements Product 102 | @owner(service: "books") 103 | @key(fields: "upc" service: "books") 104 | @key(fields: "upc" service: "reviews") 105 | { 106 | upc: String! 107 | name: String! 108 | price: Int! 109 | reviews: [Review]! @resolve(service: "reviews") 110 | 111 | isbn: String! 112 | issuer: String! 113 | publishDate: DateTime! 114 | } 115 | 116 | type Car implements Product 117 | @owner(service: "cars") 118 | @key(fields: "upc" service: "cars") 119 | @key(fields: "upc" service: "reviews") 120 | { 121 | upc: String! 122 | name: String! 123 | price: Int! 124 | reviews: [Review]! @resolve(service: "reviews") 125 | 126 | brand: String! 127 | power: Int! 128 | torque: Int! 129 | } 130 | 131 | type Review 132 | @owner(service: "reviews") 133 | { 134 | body: String! 135 | author: User! 136 | product: Product! 137 | attachment: Attachment 138 | } 139 | 140 | union Attachment = Text | Image | Audio 141 | 142 | type Text 143 | @owner(service: "reviews") 144 | @key(fields: "id" service:"reviews") 145 | { 146 | id: ID! 147 | content: String! 148 | } 149 | 150 | type Image 151 | @owner(service: "attachments") 152 | @key(fields: "id" service:"attachments") 153 | { 154 | id: ID! 155 | width: Int! 156 | height: Int! 157 | data: String! 158 | } 159 | 160 | type Audio 161 | @owner(service: "attachments") 162 | @key(fields: "id" service:"attachments") 163 | { 164 | id: ID! 165 | duration: Float! 166 | data: String! 167 | } 168 | -------------------------------------------------------------------------------- /crates/planner/tests/test.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use globset::GlobBuilder; 4 | use graphgate_planner::PlanBuilder; 5 | use graphgate_schema::ComposedSchema; 6 | 7 | #[test] 8 | fn test() { 9 | let schema = ComposedSchema::parse(include_str!("test.graphql")).unwrap(); 10 | let glob = GlobBuilder::new("./tests/*.txt") 11 | .literal_separator(true) 12 | .build() 13 | .unwrap() 14 | .compile_matcher(); 15 | 16 | for entry in fs::read_dir("./tests").unwrap() { 17 | let entry = entry.unwrap(); 18 | if !glob.is_match(entry.path()) { 19 | continue; 20 | } 21 | 22 | println!("{}", entry.path().display()); 23 | 24 | let data = fs::read_to_string(&entry.path()).unwrap(); 25 | let mut s = data.split("---"); 26 | let mut n = 1; 27 | 28 | loop { 29 | println!("\tIndex: {}", n); 30 | let graphql = match s.next() { 31 | Some(graphql) => graphql, 32 | None => break, 33 | }; 34 | let variables = s.next().unwrap(); 35 | let planner_json = s.next().unwrap(); 36 | 37 | let document = parser::parse_query(graphql).unwrap(); 38 | let builder = PlanBuilder::new(&schema, document) 39 | .variables(serde_json::from_str(variables).unwrap()); 40 | let expect_node: serde_json::Value = serde_json::from_str(planner_json).unwrap(); 41 | let actual_node = serde_json::to_value(&builder.plan().unwrap()).unwrap(); 42 | 43 | // assert_eq!( 44 | // serde_json::to_string_pretty(&actual_node).unwrap(), 45 | // serde_json::to_string_pretty(&expect_node).unwrap(), 46 | // ); 47 | assert_eq!(actual_node, expect_node); 48 | 49 | n += 1; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /crates/planner/tests/variables.txt: -------------------------------------------------------------------------------- 1 | query($u1: ID!, $u2: ID!) { 2 | u1: user(id: $u1) { 3 | id username 4 | } 5 | u2: user(id: $u2) { 6 | id username 7 | } 8 | } 9 | --- 10 | { 11 | "u1": "user1", 12 | "u2": "user2" 13 | } 14 | --- 15 | { 16 | "type": "fetch", 17 | "service": "accounts", 18 | "variables": { 19 | "u1": "user1", 20 | "u2": "user2" 21 | }, 22 | "query": "query($u1: ID!, $u2: ID!)\n{ u1:user(id: $u1) { id username } u2:user(id: $u2) { id username } }" 23 | } 24 | --- 25 | fragment A on Query { 26 | user(id: $id) { 27 | id username 28 | } 29 | } 30 | 31 | query($id: ID!) { 32 | ... A 33 | } 34 | --- 35 | { 36 | "id": "user1" 37 | } 38 | --- 39 | { 40 | "type": "fetch", 41 | "service": "accounts", 42 | "variables": { 43 | "id": "user1" 44 | }, 45 | "query": "query($id: ID!)\n{ user(id: $id) { id username } }" 46 | } 47 | --- 48 | query($id: ID!) { 49 | ... { 50 | user(id: $id) { 51 | id username 52 | } 53 | } 54 | } 55 | --- 56 | { 57 | "id": "user1" 58 | } 59 | --- 60 | { 61 | "type": "fetch", 62 | "service": "accounts", 63 | "variables": { 64 | "id": "user1" 65 | }, 66 | "query": "query($id: ID!)\n{ user(id: $id) { id username } }" 67 | } 68 | -------------------------------------------------------------------------------- /crates/schema/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphgate-schema" 3 | version = "0.5.1" 4 | authors = ["Sunli <scott_s829@163.com>"] 5 | edition = "2018" 6 | description = "GraphGate is Apollo Federation implemented in Rust" 7 | license = "MIT/Apache-2.0" 8 | homepage = "https://github.com/async-graphql/graphgate" 9 | repository = "https://github.com/async-graphql/graphgate" 10 | keywords = ["gateway", "graphql", "federation"] 11 | 12 | [dependencies] 13 | parser = { version = "3.0.24", package = "async-graphql-parser" } 14 | value = { version = "3.0.24", package = "async-graphql-value" } 15 | thiserror = "1.0.30" 16 | indexmap = { version = "1.8.0", features = ["serde-1"] } 17 | -------------------------------------------------------------------------------- /crates/schema/src/builtin.graphql: -------------------------------------------------------------------------------- 1 | """ 2 | The `Int` scalar type represents non-fractional whole numeric values. 3 | """ 4 | scalar Int 5 | 6 | """ 7 | The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point). 8 | """ 9 | scalar Float 10 | 11 | """ 12 | The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text. 13 | """ 14 | scalar String 15 | 16 | """ 17 | The `Boolean` scalar type represents `true` or `false`. 18 | """ 19 | scalar Boolean 20 | 21 | """ 22 | ID scalar 23 | """ 24 | scalar ID 25 | 26 | """ 27 | Directs the executor to include this field or fragment only when the `if` argument is true. 28 | """ 29 | directive @include("Included when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 30 | 31 | """ 32 | Directs the executor to skip this field or fragment when the `if` argument is true. 33 | """ 34 | directive @skip("Skipped when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT 35 | 36 | """ 37 | A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies. 38 | """ 39 | enum __DirectiveLocation { 40 | "Location adjacent to a query operation." 41 | QUERY 42 | 43 | "Location adjacent to a mutation operation." 44 | MUTATION 45 | 46 | "Location adjacent to a subscription operation." 47 | SUBSCRIPTION 48 | 49 | "Location adjacent to a field." 50 | FIELD 51 | 52 | "Location adjacent to a fragment definition." 53 | FRAGMENT_DEFINITION 54 | 55 | "Location adjacent to a fragment spread." 56 | FRAGMENT_SPREAD 57 | 58 | "Location adjacent to an inline fragment." 59 | INLINE_FRAGMENT 60 | 61 | "Location adjacent to a variable definition." 62 | VARIABLE_DEFINITION 63 | 64 | "Location adjacent to a schema definition." 65 | SCHEMA 66 | 67 | "Location adjacent to a scalar definition." 68 | SCALAR 69 | 70 | "Location adjacent to an object type definition." 71 | OBJECT 72 | 73 | "Location adjacent to a field definition." 74 | FIELD_DEFINITION 75 | 76 | "Location adjacent to an argument definition." 77 | ARGUMENT_DEFINITION 78 | 79 | "Location adjacent to an interface definition." 80 | INTERFACE 81 | 82 | "Location adjacent to a union definition." 83 | UNION 84 | 85 | "Location adjacent to an enum definition." 86 | ENUM 87 | 88 | "Location adjacent to an enum value definition." 89 | ENUM_VALUE 90 | 91 | "Location adjacent to an input object type definition." 92 | INPUT_OBJECT 93 | 94 | "Location adjacent to an input object field definition." 95 | INPUT_FIELD_DEFINITION 96 | } 97 | 98 | """ 99 | A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. 100 | """ 101 | type __Directive { 102 | name: String! 103 | description: String 104 | locations: [__DirectiveLocation!]! 105 | args: [__InputValue!]! 106 | } 107 | 108 | """ 109 | One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string. 110 | """ 111 | type __EnumValue { 112 | name: String! 113 | description: String 114 | isDeprecated: Boolean! 115 | deprecationReason: String 116 | } 117 | 118 | """ 119 | Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type. 120 | """ 121 | type __Field { 122 | name: String! 123 | description: String 124 | args: [__InputValue!]! 125 | type: __Type! 126 | isDeprecated: Boolean! 127 | deprecationReason: String 128 | } 129 | 130 | """ 131 | Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value. 132 | """ 133 | type __InputValue { 134 | name: String! 135 | description: String 136 | type: __Type! 137 | defaultValue: String 138 | } 139 | 140 | """ 141 | An enum describing what kind of type a given `__Type` is. 142 | """ 143 | enum __TypeKind { 144 | "Indicates this type is a scalar." 145 | Scalar 146 | 147 | "Indicates this type is an object. `fields` and `interfaces` are valid fields." 148 | Object 149 | 150 | "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields." 151 | Interface 152 | 153 | "Indicates this type is a union. `possibleTypes` is a valid field." 154 | Union 155 | 156 | "Indicates this type is an enum. `enumValues` is a valid field." 157 | Enum 158 | 159 | "Indicates this type is an input object. `inputFields` is a valid field." 160 | InputObject 161 | 162 | "Indicates this type is a list. `ofType` is a valid field." 163 | List 164 | 165 | "Indicates this type is a non-null. `ofType` is a valid field." 166 | NonNull 167 | } 168 | 169 | """ 170 | The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum. 171 | 172 | Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types. 173 | """ 174 | type __Type { 175 | kind: __TypeKind 176 | name: String 177 | description: String 178 | fields(includeDeprecated: Boolean! = false): [__Field!] 179 | interfaces: [__Type!] 180 | possibleTypes: [__Type!] 181 | enumValues(includeDeprecated: Boolean! = false): [__EnumValue!] 182 | inputFields: [__InputValue!] 183 | ofType: __Type 184 | } 185 | 186 | """ 187 | A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. 188 | """ 189 | type __Schema { 190 | types: [__Type!] 191 | queryType: __Type! 192 | mutationType: __Type 193 | subscriptionType: __Type 194 | directives: [__Directive!]! 195 | } 196 | -------------------------------------------------------------------------------- /crates/schema/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | pub enum CombineError { 5 | #[error("Redefining the schema is not allowed.")] 6 | SchemaIsNotAllowed, 7 | 8 | #[error("Type '{type_name}' definition conflicted.")] 9 | DefinitionConflicted { type_name: String }, 10 | 11 | #[error("Field '{type_name}.{field_name}' definition conflicted.")] 12 | FieldConflicted { 13 | type_name: String, 14 | field_name: String, 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /crates/schema/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | mod composed_schema; 4 | mod error; 5 | mod type_ext; 6 | mod value_ext; 7 | 8 | pub use composed_schema::{ 9 | ComposedSchema, Deprecation, KeyFields, MetaEnumValue, MetaField, MetaInputValue, MetaType, 10 | TypeKind, 11 | }; 12 | pub use error::CombineError; 13 | pub use type_ext::TypeExt; 14 | pub use value_ext::ValueExt; 15 | -------------------------------------------------------------------------------- /crates/schema/src/type_ext.rs: -------------------------------------------------------------------------------- 1 | use parser::types::{BaseType, Type}; 2 | 3 | pub trait TypeExt { 4 | fn concrete_typename(&self) -> &str; 5 | fn is_subtype(&self, sub: &Type) -> bool; 6 | } 7 | 8 | impl TypeExt for Type { 9 | fn concrete_typename(&self) -> &str { 10 | match &self.base { 11 | BaseType::Named(name) => name.as_str(), 12 | BaseType::List(ty) => ty.concrete_typename(), 13 | } 14 | } 15 | 16 | fn is_subtype(&self, sub: &Type) -> bool { 17 | if !sub.nullable || self.nullable { 18 | match (&self.base, &sub.base) { 19 | (BaseType::Named(super_type), BaseType::Named(sub_type)) => super_type == sub_type, 20 | (BaseType::List(super_type), BaseType::List(sub_type)) => { 21 | super_type.is_subtype(sub_type) 22 | } 23 | _ => false, 24 | } 25 | } else { 26 | false 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/schema/src/value_ext.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexSet; 2 | use value::Value; 3 | 4 | pub trait ValueExt { 5 | fn referenced_variables(&self) -> IndexSet<&str>; 6 | } 7 | 8 | impl ValueExt for Value { 9 | fn referenced_variables(&self) -> IndexSet<&str> { 10 | pub fn referenced_variables_to_set<'a>(value: &'a Value, vars: &mut IndexSet<&'a str>) { 11 | match value { 12 | Value::Variable(name) => { 13 | vars.insert(name); 14 | } 15 | Value::List(values) => values 16 | .iter() 17 | .for_each(|value| referenced_variables_to_set(value, vars)), 18 | Value::Object(obj) => obj 19 | .values() 20 | .for_each(|value| referenced_variables_to_set(value, vars)), 21 | _ => {} 22 | } 23 | } 24 | 25 | let mut vars = IndexSet::new(); 26 | referenced_variables_to_set(self, &mut vars); 27 | vars 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/validation/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "graphgate-validation" 3 | version = "0.5.0" 4 | authors = ["Sunli <scott_s829@163.com>"] 5 | edition = "2018" 6 | description = "GraphGate is Apollo Federation implemented in Rust" 7 | license = "MIT/Apache-2.0" 8 | homepage = "https://github.com/async-graphql/graphgate" 9 | repository = "https://github.com/async-graphql/graphgate" 10 | keywords = ["gateway", "graphql", "federation"] 11 | 12 | [dependencies] 13 | graphgate-schema = { version = "0.5.0", path = "../schema" } 14 | 15 | parser = { version = "3.0.24", package = "async-graphql-parser" } 16 | value = { version = "3.0.24", package = "async-graphql-value" } 17 | indexmap = { version = "1.8.0", features = ["serde-1"] } 18 | 19 | [dev-dependencies] 20 | once_cell = "1.9.0" 21 | -------------------------------------------------------------------------------- /crates/validation/src/error.rs: -------------------------------------------------------------------------------- 1 | use parser::Pos; 2 | 3 | #[derive(Debug)] 4 | pub struct RuleError { 5 | pub message: String, 6 | pub locations: Vec<Pos>, 7 | } 8 | -------------------------------------------------------------------------------- /crates/validation/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | #[cfg(test)] 4 | #[macro_use] 5 | mod test_harness; 6 | 7 | mod error; 8 | mod rules; 9 | mod suggestion; 10 | mod utils; 11 | mod visitor; 12 | 13 | use graphgate_schema::ComposedSchema; 14 | use parser::types::ExecutableDocument; 15 | use value::Variables; 16 | 17 | use visitor::{visit, Visitor, VisitorContext, VisitorNil}; 18 | 19 | pub use error::RuleError; 20 | 21 | macro_rules! rules { 22 | ($($rule:ident),*) => { 23 | VisitorNil$(.with(rules::$rule::default()))* 24 | }; 25 | } 26 | 27 | pub fn check_rules( 28 | composed_schema: &ComposedSchema, 29 | document: &ExecutableDocument, 30 | variables: &Variables, 31 | ) -> Vec<RuleError> { 32 | let mut ctx = VisitorContext::new(composed_schema, document, variables); 33 | let mut visitor = rules!( 34 | ArgumentsOfCorrectType, 35 | DefaultValuesOfCorrectType, 36 | FieldsOnCorrectType, 37 | FragmentsOnCompositeTypes, 38 | KnownArgumentNames, 39 | KnownDirectives, 40 | KnownFragmentNames, 41 | KnownTypeNames, 42 | NoFragmentCycles, 43 | NoUndefinedVariables, 44 | NoUnusedVariables, 45 | NoUnusedFragments, 46 | OverlappingFieldsCanBeMerged, 47 | PossibleFragmentSpreads, 48 | ProvidedNonNullArguments, 49 | ScalarLeafs, 50 | UniqueArgumentNames, 51 | UniqueVariableNames, 52 | VariablesAreInputTypes, 53 | VariableInAllowedPosition 54 | ); 55 | visit(&mut visitor, &mut ctx, &document); 56 | ctx.errors 57 | } 58 | -------------------------------------------------------------------------------- /crates/validation/src/rules/default_values_of_correct_type.rs: -------------------------------------------------------------------------------- 1 | use parser::types::VariableDefinition; 2 | use parser::Positioned; 3 | 4 | use crate::utils::{is_valid_input_value, PathNode}; 5 | use crate::{Visitor, VisitorContext}; 6 | 7 | #[derive(Default)] 8 | pub struct DefaultValuesOfCorrectType; 9 | 10 | impl<'a> Visitor<'a> for DefaultValuesOfCorrectType { 11 | fn enter_variable_definition( 12 | &mut self, 13 | ctx: &mut VisitorContext<'a>, 14 | variable_definition: &'a Positioned<VariableDefinition>, 15 | ) { 16 | if let Some(value) = &variable_definition.node.default_value { 17 | if !variable_definition.node.var_type.node.nullable { 18 | ctx.report_error(vec![variable_definition.pos],format!( 19 | "Argument \"{}\" has type \"{}\" and is not nullable, so it can't have a default value", 20 | variable_definition.node.name, variable_definition.node.var_type, 21 | )); 22 | } else if let Some(reason) = is_valid_input_value( 23 | ctx.schema, 24 | &variable_definition.node.var_type.node, 25 | &value.node, 26 | PathNode::new(&variable_definition.node.name.node), 27 | ) { 28 | ctx.report_error( 29 | vec![variable_definition.pos], 30 | format!("Invalid default value for argument {}", reason), 31 | ) 32 | } 33 | } 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use super::*; 40 | 41 | pub fn factory() -> DefaultValuesOfCorrectType { 42 | DefaultValuesOfCorrectType 43 | } 44 | 45 | #[test] 46 | fn variables_with_no_default_values() { 47 | expect_passes_rule!( 48 | factory, 49 | r#" 50 | query NullableValues($a: Int, $b: String, $c: ComplexInput) { 51 | dog { name } 52 | } 53 | "#, 54 | ); 55 | } 56 | 57 | #[test] 58 | fn required_variables_without_default_values() { 59 | expect_passes_rule!( 60 | factory, 61 | r#" 62 | query RequiredValues($a: Int!, $b: String!) { 63 | dog { name } 64 | } 65 | "#, 66 | ); 67 | } 68 | 69 | #[test] 70 | fn variables_with_valid_default_values() { 71 | expect_passes_rule!( 72 | factory, 73 | r#" 74 | query WithDefaultValues( 75 | $a: Int = 1, 76 | $b: String = "ok", 77 | $c: ComplexInput = { requiredField: true, intField: 3 } 78 | ) { 79 | dog { name } 80 | } 81 | "#, 82 | ); 83 | } 84 | 85 | #[test] 86 | fn no_required_variables_with_default_values() { 87 | expect_fails_rule!( 88 | factory, 89 | r#" 90 | query UnreachableDefaultValues($a: Int! = 3, $b: String! = "default") { 91 | dog { name } 92 | } 93 | "#, 94 | ); 95 | } 96 | 97 | #[test] 98 | fn variables_with_invalid_default_values() { 99 | expect_fails_rule!( 100 | factory, 101 | r#" 102 | query InvalidDefaultValues( 103 | $a: Int = "one", 104 | $b: String = 4, 105 | $c: ComplexInput = "notverycomplex" 106 | ) { 107 | dog { name } 108 | } 109 | "#, 110 | ); 111 | } 112 | 113 | #[test] 114 | fn complex_variables_missing_required_field() { 115 | expect_fails_rule!( 116 | factory, 117 | r#" 118 | query MissingRequiredField($a: ComplexInput = {intField: 3}) { 119 | dog { name } 120 | } 121 | "#, 122 | ); 123 | } 124 | 125 | #[test] 126 | fn list_variables_with_invalid_item() { 127 | expect_fails_rule!( 128 | factory, 129 | r#" 130 | query InvalidItem($a: [String] = ["one", 2]) { 131 | dog { name } 132 | } 133 | "#, 134 | ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /crates/validation/src/rules/fields_on_correct_type.rs: -------------------------------------------------------------------------------- 1 | use parser::types::Field; 2 | use parser::Positioned; 3 | 4 | use graphgate_schema::TypeKind; 5 | 6 | use crate::suggestion::make_suggestion; 7 | use crate::{Visitor, VisitorContext}; 8 | 9 | #[derive(Default)] 10 | pub struct FieldsOnCorrectType; 11 | 12 | impl<'a> Visitor<'a> for FieldsOnCorrectType { 13 | fn enter_field(&mut self, ctx: &mut VisitorContext<'a>, field: &'a Positioned<Field>) { 14 | if let Some(parent_type) = ctx.parent_type() { 15 | if matches!(parent_type.kind, TypeKind::Union | TypeKind::Enum) 16 | && field.node.name.node == "__typename" 17 | { 18 | return; 19 | } 20 | 21 | if !parent_type.fields.contains_key(&field.node.name.node) { 22 | ctx.report_error( 23 | vec![field.pos], 24 | format!( 25 | "Unknown field \"{}\" on type \"{}\".{}", 26 | field.node.name, 27 | parent_type.name, 28 | make_suggestion( 29 | " Did you mean", 30 | parent_type.fields.keys().map(|name| name.as_str()), 31 | &field.node.name.node, 32 | ) 33 | .unwrap_or_default() 34 | ), 35 | ); 36 | } 37 | } 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | 45 | pub fn factory() -> FieldsOnCorrectType { 46 | FieldsOnCorrectType 47 | } 48 | 49 | #[test] 50 | fn selection_on_object() { 51 | expect_passes_rule!( 52 | factory, 53 | r#" 54 | fragment objectFieldSelection on Dog { 55 | __typename 56 | name 57 | } 58 | { __typename } 59 | "#, 60 | ); 61 | } 62 | 63 | #[test] 64 | fn aliased_selection_on_object() { 65 | expect_passes_rule!( 66 | factory, 67 | r#" 68 | fragment aliasedObjectFieldSelection on Dog { 69 | tn : __typename 70 | otherName : name 71 | } 72 | { __typename } 73 | "#, 74 | ); 75 | } 76 | 77 | #[test] 78 | fn selection_on_interface() { 79 | expect_passes_rule!( 80 | factory, 81 | r#" 82 | fragment interfaceFieldSelection on Pet { 83 | __typename 84 | name 85 | } 86 | { __typename } 87 | "#, 88 | ); 89 | } 90 | 91 | #[test] 92 | fn aliased_selection_on_interface() { 93 | expect_passes_rule!( 94 | factory, 95 | r#" 96 | fragment interfaceFieldSelection on Pet { 97 | otherName : name 98 | } 99 | { __typename } 100 | "#, 101 | ); 102 | } 103 | 104 | #[test] 105 | fn lying_alias_selection() { 106 | expect_passes_rule!( 107 | factory, 108 | r#" 109 | fragment lyingAliasSelection on Dog { 110 | name : nickname 111 | } 112 | { __typename } 113 | "#, 114 | ); 115 | } 116 | 117 | #[test] 118 | fn ignores_unknown_type() { 119 | expect_passes_rule!( 120 | factory, 121 | r#" 122 | fragment unknownSelection on UnknownType { 123 | unknownField 124 | } 125 | { __typename } 126 | "#, 127 | ); 128 | } 129 | 130 | #[test] 131 | fn nested_unknown_fields() { 132 | expect_fails_rule!( 133 | factory, 134 | r#" 135 | fragment typeKnownAgain on Pet { 136 | unknown_pet_field { 137 | ... on Cat { 138 | unknown_cat_field 139 | } 140 | } 141 | } 142 | { __typename } 143 | "#, 144 | ); 145 | } 146 | 147 | #[test] 148 | fn unknown_field_on_fragment() { 149 | expect_fails_rule!( 150 | factory, 151 | r#" 152 | fragment fieldNotDefined on Dog { 153 | meowVolume 154 | } 155 | { __typename } 156 | "#, 157 | ); 158 | } 159 | 160 | #[test] 161 | fn ignores_deeply_unknown_field() { 162 | expect_fails_rule!( 163 | factory, 164 | r#" 165 | fragment deepFieldNotDefined on Dog { 166 | unknown_field { 167 | deeper_unknown_field 168 | } 169 | } 170 | { __typename } 171 | "#, 172 | ); 173 | } 174 | 175 | #[test] 176 | fn unknown_subfield() { 177 | expect_fails_rule!( 178 | factory, 179 | r#" 180 | fragment subFieldNotDefined on Human { 181 | pets { 182 | unknown_field 183 | } 184 | } 185 | { __typename } 186 | "#, 187 | ); 188 | } 189 | 190 | #[test] 191 | fn unknown_field_on_inline_fragment() { 192 | expect_fails_rule!( 193 | factory, 194 | r#" 195 | fragment fieldNotDefined on Pet { 196 | ... on Dog { 197 | meowVolume 198 | } 199 | } 200 | { __typename } 201 | "#, 202 | ); 203 | } 204 | 205 | #[test] 206 | fn unknown_aliased_target() { 207 | expect_fails_rule!( 208 | factory, 209 | r#" 210 | fragment aliasedFieldTargetNotDefined on Dog { 211 | volume : mooVolume 212 | } 213 | { __typename } 214 | "#, 215 | ); 216 | } 217 | 218 | #[test] 219 | fn unknown_aliased_lying_field_target() { 220 | expect_fails_rule!( 221 | factory, 222 | r#" 223 | fragment aliasedLyingFieldTargetNotDefined on Dog { 224 | barkVolume : kawVolume 225 | } 226 | { __typename } 227 | "#, 228 | ); 229 | } 230 | 231 | #[test] 232 | fn not_defined_on_interface() { 233 | expect_fails_rule!( 234 | factory, 235 | r#" 236 | fragment notDefinedOnInterface on Pet { 237 | tailLength 238 | } 239 | { __typename } 240 | "#, 241 | ); 242 | } 243 | 244 | #[test] 245 | fn defined_in_concrete_types_but_not_interface() { 246 | expect_fails_rule!( 247 | factory, 248 | r#" 249 | fragment definedOnImplementorsButNotInterface on Pet { 250 | nickname 251 | } 252 | { __typename } 253 | "#, 254 | ); 255 | } 256 | 257 | #[test] 258 | fn meta_field_on_union() { 259 | expect_passes_rule!( 260 | factory, 261 | r#" 262 | fragment definedOnImplementorsButNotInterface on Pet { 263 | __typename 264 | } 265 | { __typename } 266 | "#, 267 | ); 268 | } 269 | 270 | #[test] 271 | fn fields_on_union() { 272 | expect_fails_rule!( 273 | factory, 274 | r#" 275 | fragment definedOnImplementorsQueriedOnUnion on CatOrDog { 276 | name 277 | } 278 | { __typename } 279 | "#, 280 | ); 281 | } 282 | 283 | #[test] 284 | fn typename_on_union() { 285 | expect_passes_rule!( 286 | factory, 287 | r#" 288 | fragment objectFieldSelection on Pet { 289 | __typename 290 | ... on Dog { 291 | name 292 | } 293 | ... on Cat { 294 | name 295 | } 296 | } 297 | { __typename } 298 | "#, 299 | ); 300 | } 301 | 302 | #[test] 303 | fn valid_field_in_inline_fragment() { 304 | expect_passes_rule!( 305 | factory, 306 | r#" 307 | fragment objectFieldSelection on Pet { 308 | ... on Dog { 309 | name 310 | } 311 | ... { 312 | name 313 | } 314 | } 315 | { __typename } 316 | "#, 317 | ); 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /crates/validation/src/rules/fragments_on_composite_types.rs: -------------------------------------------------------------------------------- 1 | use parser::types::{FragmentDefinition, InlineFragment}; 2 | use parser::Positioned; 3 | use value::Name; 4 | 5 | use crate::{Visitor, VisitorContext}; 6 | 7 | #[derive(Default)] 8 | pub struct FragmentsOnCompositeTypes; 9 | 10 | impl<'a> Visitor<'a> for FragmentsOnCompositeTypes { 11 | fn enter_fragment_definition( 12 | &mut self, 13 | ctx: &mut VisitorContext<'a>, 14 | name: &'a Name, 15 | fragment_definition: &'a Positioned<FragmentDefinition>, 16 | ) { 17 | if let Some(current_type) = ctx.current_type() { 18 | if !current_type.is_composite() { 19 | ctx.report_error( 20 | vec![fragment_definition.pos], 21 | format!( 22 | "Fragment \"{}\" cannot condition non composite type \"{}\"", 23 | name, fragment_definition.node.type_condition.node.on.node, 24 | ), 25 | ); 26 | } 27 | } 28 | } 29 | 30 | fn enter_inline_fragment( 31 | &mut self, 32 | ctx: &mut VisitorContext<'a>, 33 | inline_fragment: &'a Positioned<InlineFragment>, 34 | ) { 35 | if let Some(current_type) = ctx.current_type() { 36 | if !current_type.is_composite() { 37 | ctx.report_error( 38 | vec![inline_fragment.pos], 39 | format!( 40 | "Fragment cannot condition non composite type \"{}\"", 41 | current_type.name 42 | ), 43 | ); 44 | } 45 | } 46 | } 47 | } 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | use super::*; 52 | 53 | fn factory() -> FragmentsOnCompositeTypes { 54 | FragmentsOnCompositeTypes 55 | } 56 | 57 | #[test] 58 | fn on_object() { 59 | expect_passes_rule!( 60 | factory, 61 | r#" 62 | fragment validFragment on Dog { 63 | barks 64 | } 65 | { __typename } 66 | "#, 67 | ); 68 | } 69 | 70 | #[test] 71 | fn on_interface() { 72 | expect_passes_rule!( 73 | factory, 74 | r#" 75 | fragment validFragment on Pet { 76 | name 77 | } 78 | { __typename } 79 | "#, 80 | ); 81 | } 82 | 83 | #[test] 84 | fn on_object_inline() { 85 | expect_passes_rule!( 86 | factory, 87 | r#" 88 | fragment validFragment on Pet { 89 | ... on Dog { 90 | barks 91 | } 92 | } 93 | { __typename } 94 | "#, 95 | ); 96 | } 97 | 98 | #[test] 99 | fn on_inline_without_type_cond() { 100 | expect_passes_rule!( 101 | factory, 102 | r#" 103 | fragment validFragment on Pet { 104 | ... { 105 | name 106 | } 107 | } 108 | { __typename } 109 | "#, 110 | ); 111 | } 112 | 113 | #[test] 114 | fn on_union() { 115 | expect_passes_rule!( 116 | factory, 117 | r#" 118 | fragment validFragment on CatOrDog { 119 | __typename 120 | } 121 | { __typename } 122 | "#, 123 | ); 124 | } 125 | 126 | #[test] 127 | fn not_on_scalar() { 128 | expect_fails_rule!( 129 | factory, 130 | r#" 131 | fragment scalarFragment on Boolean { 132 | bad 133 | } 134 | { __typename } 135 | "#, 136 | ); 137 | } 138 | 139 | #[test] 140 | fn not_on_enum() { 141 | expect_fails_rule!( 142 | factory, 143 | r#" 144 | fragment scalarFragment on FurColor { 145 | bad 146 | } 147 | { __typename } 148 | "#, 149 | ); 150 | } 151 | 152 | #[test] 153 | fn not_on_input_object() { 154 | expect_fails_rule!( 155 | factory, 156 | r#" 157 | fragment inputFragment on ComplexInput { 158 | stringField 159 | } 160 | { __typename } 161 | "#, 162 | ); 163 | } 164 | 165 | #[test] 166 | fn not_on_scalar_inline() { 167 | expect_fails_rule!( 168 | factory, 169 | r#" 170 | fragment invalidFragment on Pet { 171 | ... on String { 172 | barks 173 | } 174 | } 175 | { __typename } 176 | "#, 177 | ); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /crates/validation/src/rules/known_argument_names.rs: -------------------------------------------------------------------------------- 1 | use graphgate_schema::MetaInputValue; 2 | use indexmap::IndexMap; 3 | use parser::types::{Directive, Field}; 4 | use parser::Positioned; 5 | use value::{Name, Value}; 6 | 7 | use crate::suggestion::make_suggestion; 8 | use crate::{Visitor, VisitorContext}; 9 | 10 | enum ArgsType<'a> { 11 | Directive(&'a str), 12 | Field { 13 | field_name: &'a str, 14 | type_name: &'a str, 15 | }, 16 | } 17 | 18 | #[derive(Default)] 19 | pub struct KnownArgumentNames<'a> { 20 | current_args: Option<(&'a IndexMap<Name, MetaInputValue>, ArgsType<'a>)>, 21 | } 22 | 23 | impl<'a> KnownArgumentNames<'a> { 24 | fn get_suggestion(&self, name: &str) -> String { 25 | make_suggestion( 26 | " Did you mean", 27 | self.current_args 28 | .iter() 29 | .map(|(args, _)| args.iter().map(|arg| arg.0.as_str())) 30 | .flatten(), 31 | name, 32 | ) 33 | .unwrap_or_default() 34 | } 35 | } 36 | 37 | impl<'a> Visitor<'a> for KnownArgumentNames<'a> { 38 | fn enter_directive( 39 | &mut self, 40 | ctx: &mut VisitorContext<'a>, 41 | directive: &'a Positioned<Directive>, 42 | ) { 43 | self.current_args = ctx 44 | .schema 45 | .directives 46 | .get(directive.node.name.node.as_str()) 47 | .map(|d| (&d.arguments, ArgsType::Directive(&directive.node.name.node))); 48 | } 49 | 50 | fn exit_directive( 51 | &mut self, 52 | _ctx: &mut VisitorContext<'a>, 53 | _directive: &'a Positioned<Directive>, 54 | ) { 55 | self.current_args = None; 56 | } 57 | 58 | fn enter_argument( 59 | &mut self, 60 | ctx: &mut VisitorContext<'a>, 61 | name: &'a Positioned<Name>, 62 | _value: &'a Positioned<Value>, 63 | ) { 64 | if let Some((args, arg_type)) = &self.current_args { 65 | if !args.contains_key(name.node.as_str()) { 66 | match arg_type { 67 | ArgsType::Field { 68 | field_name, 69 | type_name, 70 | } => { 71 | ctx.report_error( 72 | vec![name.pos], 73 | format!( 74 | "Unknown argument \"{}\" on field \"{}\" of type \"{}\".{}", 75 | name, 76 | field_name, 77 | type_name, 78 | self.get_suggestion(name.node.as_str()) 79 | ), 80 | ); 81 | } 82 | ArgsType::Directive(directive_name) => { 83 | ctx.report_error( 84 | vec![name.pos], 85 | format!( 86 | "Unknown argument \"{}\" on directive \"{}\".{}", 87 | name, 88 | directive_name, 89 | self.get_suggestion(name.node.as_str()) 90 | ), 91 | ); 92 | } 93 | } 94 | } 95 | } 96 | } 97 | 98 | fn enter_field(&mut self, ctx: &mut VisitorContext<'a>, field: &'a Positioned<Field>) { 99 | if let Some(parent_type) = ctx.parent_type() { 100 | if let Some(schema_field) = parent_type.field_by_name(&field.node.name.node) { 101 | self.current_args = Some(( 102 | &schema_field.arguments, 103 | ArgsType::Field { 104 | field_name: &field.node.name.node, 105 | type_name: &ctx.parent_type().unwrap().name, 106 | }, 107 | )); 108 | } 109 | } 110 | } 111 | 112 | fn exit_field(&mut self, _ctx: &mut VisitorContext<'a>, _field: &'a Positioned<Field>) { 113 | self.current_args = None; 114 | } 115 | } 116 | 117 | #[cfg(test)] 118 | mod tests { 119 | use super::*; 120 | 121 | pub fn factory<'a>() -> KnownArgumentNames<'a> { 122 | KnownArgumentNames::default() 123 | } 124 | 125 | #[test] 126 | fn single_arg_is_known() { 127 | expect_passes_rule!( 128 | factory, 129 | r#" 130 | fragment argOnRequiredArg on Dog { 131 | doesKnowCommand(dogCommand: SIT) 132 | } 133 | { __typename } 134 | "#, 135 | ); 136 | } 137 | 138 | #[test] 139 | fn multiple_args_are_known() { 140 | expect_passes_rule!( 141 | factory, 142 | r#" 143 | fragment multipleArgs on ComplicatedArgs { 144 | multipleReqs(req1: 1, req2: 2) 145 | } 146 | { __typename } 147 | "#, 148 | ); 149 | } 150 | 151 | #[test] 152 | fn ignores_args_of_unknown_fields() { 153 | expect_passes_rule!( 154 | factory, 155 | r#" 156 | fragment argOnUnknownField on Dog { 157 | unknownField(unknownArg: SIT) 158 | } 159 | { __typename } 160 | "#, 161 | ); 162 | } 163 | 164 | #[test] 165 | fn multiple_args_in_reverse_order_are_known() { 166 | expect_passes_rule!( 167 | factory, 168 | r#" 169 | fragment multipleArgsReverseOrder on ComplicatedArgs { 170 | multipleReqs(req2: 2, req1: 1) 171 | } 172 | { __typename } 173 | "#, 174 | ); 175 | } 176 | 177 | #[test] 178 | fn no_args_on_optional_arg() { 179 | expect_passes_rule!( 180 | factory, 181 | r#" 182 | fragment noArgOnOptionalArg on Dog { 183 | isHousetrained 184 | } 185 | { __typename } 186 | "#, 187 | ); 188 | } 189 | 190 | #[test] 191 | fn args_are_known_deeply() { 192 | expect_passes_rule!( 193 | factory, 194 | r#" 195 | { 196 | dog { 197 | doesKnowCommand(dogCommand: SIT) 198 | } 199 | human { 200 | pet { 201 | ... on Dog { 202 | doesKnowCommand(dogCommand: SIT) 203 | } 204 | } 205 | } 206 | } 207 | "#, 208 | ); 209 | } 210 | 211 | #[test] 212 | fn directive_args_are_known() { 213 | expect_passes_rule!( 214 | factory, 215 | r#" 216 | { 217 | dog @skip(if: true) 218 | } 219 | "#, 220 | ); 221 | } 222 | 223 | #[test] 224 | fn undirective_args_are_invalid() { 225 | expect_fails_rule!( 226 | factory, 227 | r#" 228 | { 229 | dog @skip(unless: true) 230 | } 231 | "#, 232 | ); 233 | } 234 | 235 | #[test] 236 | fn invalid_arg_name() { 237 | expect_fails_rule!( 238 | factory, 239 | r#" 240 | fragment invalidArgName on Dog { 241 | doesKnowCommand(unknown: true) 242 | } 243 | { __typename } 244 | "#, 245 | ); 246 | } 247 | 248 | #[test] 249 | fn unknown_args_amongst_known_args() { 250 | expect_fails_rule!( 251 | factory, 252 | r#" 253 | fragment oneGoodArgOneInvalidArg on Dog { 254 | doesKnowCommand(whoknows: 1, dogCommand: SIT, unknown: true) 255 | } 256 | { __typename } 257 | "#, 258 | ); 259 | } 260 | 261 | #[test] 262 | fn unknown_args_deeply() { 263 | expect_fails_rule!( 264 | factory, 265 | r#" 266 | { 267 | dog { 268 | doesKnowCommand(unknown: true) 269 | } 270 | human { 271 | pet { 272 | ... on Dog { 273 | doesKnowCommand(unknown: true) 274 | } 275 | } 276 | } 277 | } 278 | "#, 279 | ); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /crates/validation/src/rules/known_directives.rs: -------------------------------------------------------------------------------- 1 | use parser::types::{ 2 | Directive, DirectiveLocation, Field, FragmentDefinition, FragmentSpread, InlineFragment, 3 | OperationDefinition, OperationType, 4 | }; 5 | use parser::Positioned; 6 | use value::Name; 7 | 8 | use crate::{Visitor, VisitorContext}; 9 | 10 | #[derive(Default)] 11 | pub struct KnownDirectives { 12 | location_stack: Vec<DirectiveLocation>, 13 | } 14 | 15 | impl<'a> Visitor<'a> for KnownDirectives { 16 | fn enter_operation_definition( 17 | &mut self, 18 | _ctx: &mut VisitorContext<'a>, 19 | _name: Option<&'a Name>, 20 | operation_definition: &'a Positioned<OperationDefinition>, 21 | ) { 22 | self.location_stack 23 | .push(match &operation_definition.node.ty { 24 | OperationType::Query => DirectiveLocation::Query, 25 | OperationType::Mutation => DirectiveLocation::Mutation, 26 | OperationType::Subscription => DirectiveLocation::Subscription, 27 | }); 28 | } 29 | 30 | fn exit_operation_definition( 31 | &mut self, 32 | _ctx: &mut VisitorContext<'a>, 33 | _name: Option<&'a Name>, 34 | _operation_definition: &'a Positioned<OperationDefinition>, 35 | ) { 36 | self.location_stack.pop(); 37 | } 38 | 39 | fn enter_fragment_definition( 40 | &mut self, 41 | _ctx: &mut VisitorContext<'a>, 42 | _name: &'a Name, 43 | _fragment_definition: &'a Positioned<FragmentDefinition>, 44 | ) { 45 | self.location_stack 46 | .push(DirectiveLocation::FragmentDefinition); 47 | } 48 | 49 | fn exit_fragment_definition( 50 | &mut self, 51 | _ctx: &mut VisitorContext<'a>, 52 | _name: &'a Name, 53 | _fragment_definition: &'a Positioned<FragmentDefinition>, 54 | ) { 55 | self.location_stack.pop(); 56 | } 57 | 58 | fn enter_directive( 59 | &mut self, 60 | ctx: &mut VisitorContext<'a>, 61 | directive: &'a Positioned<Directive>, 62 | ) { 63 | if let Some(schema_directive) = ctx.schema.directives.get(directive.node.name.node.as_str()) 64 | { 65 | if let Some(current_location) = self.location_stack.last() { 66 | if !schema_directive.locations.contains(current_location) { 67 | ctx.report_error( 68 | vec![directive.pos], 69 | format!( 70 | "Directive \"{}\" may not be used on \"{:?}\"", 71 | directive.node.name.node, current_location 72 | ), 73 | ) 74 | } 75 | } 76 | } else { 77 | ctx.report_error( 78 | vec![directive.pos], 79 | format!("Unknown directive \"{}\"", directive.node.name.node), 80 | ); 81 | } 82 | } 83 | 84 | fn enter_field(&mut self, _ctx: &mut VisitorContext<'a>, _field: &'a Positioned<Field>) { 85 | self.location_stack.push(DirectiveLocation::Field); 86 | } 87 | 88 | fn exit_field(&mut self, _ctx: &mut VisitorContext<'a>, _field: &'a Positioned<Field>) { 89 | self.location_stack.pop(); 90 | } 91 | 92 | fn enter_fragment_spread( 93 | &mut self, 94 | _ctx: &mut VisitorContext<'a>, 95 | _fragment_spread: &'a Positioned<FragmentSpread>, 96 | ) { 97 | self.location_stack.push(DirectiveLocation::FragmentSpread); 98 | } 99 | 100 | fn exit_fragment_spread( 101 | &mut self, 102 | _ctx: &mut VisitorContext<'a>, 103 | _fragment_spread: &'a Positioned<FragmentSpread>, 104 | ) { 105 | self.location_stack.pop(); 106 | } 107 | 108 | fn enter_inline_fragment( 109 | &mut self, 110 | _ctx: &mut VisitorContext<'a>, 111 | _inline_fragment: &'a Positioned<InlineFragment>, 112 | ) { 113 | self.location_stack.push(DirectiveLocation::InlineFragment); 114 | } 115 | 116 | fn exit_inline_fragment( 117 | &mut self, 118 | _ctx: &mut VisitorContext<'a>, 119 | _inline_fragment: &'a Positioned<InlineFragment>, 120 | ) { 121 | self.location_stack.pop(); 122 | } 123 | } 124 | 125 | #[cfg(test)] 126 | mod tests { 127 | use super::*; 128 | 129 | pub fn factory() -> KnownDirectives { 130 | KnownDirectives::default() 131 | } 132 | 133 | #[test] 134 | fn with_no_directives() { 135 | expect_passes_rule!( 136 | factory, 137 | r#" 138 | query Foo { 139 | name 140 | ...Frag 141 | } 142 | fragment Frag on Dog { 143 | name 144 | } 145 | "#, 146 | ); 147 | } 148 | 149 | #[test] 150 | fn with_known_directives() { 151 | expect_passes_rule!( 152 | factory, 153 | r#" 154 | { 155 | dog @include(if: true) { 156 | name 157 | } 158 | human @skip(if: false) { 159 | name 160 | } 161 | } 162 | "#, 163 | ); 164 | } 165 | 166 | #[test] 167 | fn with_unknown_directive() { 168 | expect_fails_rule!( 169 | factory, 170 | r#" 171 | { 172 | dog @unknown(directive: "value") { 173 | name 174 | } 175 | } 176 | "#, 177 | ); 178 | } 179 | 180 | #[test] 181 | fn with_many_unknown_directives() { 182 | expect_fails_rule!( 183 | factory, 184 | r#" 185 | { 186 | dog @unknown(directive: "value") { 187 | name 188 | } 189 | human @unknown(directive: "value") { 190 | name 191 | pets @unknown(directive: "value") { 192 | name 193 | } 194 | } 195 | } 196 | "#, 197 | ); 198 | } 199 | 200 | #[test] 201 | fn with_well_placed_directives() { 202 | expect_passes_rule!( 203 | factory, 204 | r#" 205 | query Foo { 206 | name @include(if: true) 207 | ...Frag @include(if: true) 208 | skippedField @skip(if: true) 209 | ...SkippedFrag @skip(if: true) 210 | } 211 | mutation Bar { 212 | someField 213 | } 214 | "#, 215 | ); 216 | } 217 | 218 | #[test] 219 | fn with_misplaced_directives() { 220 | expect_fails_rule!( 221 | factory, 222 | r#" 223 | query Foo @include(if: true) { 224 | name 225 | ...Frag 226 | } 227 | mutation Bar { 228 | someField 229 | } 230 | "#, 231 | ); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /crates/validation/src/rules/known_fragment_names.rs: -------------------------------------------------------------------------------- 1 | use parser::types::FragmentSpread; 2 | use parser::Positioned; 3 | 4 | use crate::{Visitor, VisitorContext}; 5 | 6 | #[derive(Default)] 7 | pub struct KnownFragmentNames; 8 | 9 | impl<'a> Visitor<'a> for KnownFragmentNames { 10 | fn enter_fragment_spread( 11 | &mut self, 12 | ctx: &mut VisitorContext<'a>, 13 | fragment_spread: &'a Positioned<FragmentSpread>, 14 | ) { 15 | if !ctx.is_known_fragment(&fragment_spread.node.fragment_name.node) { 16 | ctx.report_error( 17 | vec![fragment_spread.pos], 18 | format!( 19 | r#"Unknown fragment: "{}""#, 20 | fragment_spread.node.fragment_name.node 21 | ), 22 | ); 23 | } 24 | } 25 | } 26 | 27 | #[cfg(test)] 28 | mod tests { 29 | use super::*; 30 | 31 | pub fn factory() -> KnownFragmentNames { 32 | KnownFragmentNames::default() 33 | } 34 | 35 | #[test] 36 | fn known() { 37 | expect_passes_rule!( 38 | factory, 39 | r#" 40 | { 41 | human(id: 4) { 42 | ...HumanFields1 43 | ... on Human { 44 | ...HumanFields2 45 | } 46 | ... { 47 | name 48 | } 49 | } 50 | } 51 | fragment HumanFields1 on Human { 52 | name 53 | ...HumanFields3 54 | } 55 | fragment HumanFields2 on Human { 56 | name 57 | } 58 | fragment HumanFields3 on Human { 59 | name 60 | } 61 | "#, 62 | ); 63 | } 64 | 65 | #[test] 66 | fn unknown() { 67 | expect_fails_rule!( 68 | factory, 69 | r#" 70 | { 71 | human(id: 4) { 72 | ...UnknownFragment1 73 | ... on Human { 74 | ...UnknownFragment2 75 | } 76 | } 77 | } 78 | fragment HumanFields on Human { 79 | name 80 | ...UnknownFragment3 81 | } 82 | "#, 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /crates/validation/src/rules/known_type_names.rs: -------------------------------------------------------------------------------- 1 | use graphgate_schema::TypeExt; 2 | use parser::types::{FragmentDefinition, InlineFragment, TypeCondition, VariableDefinition}; 3 | use parser::{Pos, Positioned}; 4 | use value::Name; 5 | 6 | use crate::{Visitor, VisitorContext}; 7 | 8 | #[derive(Default)] 9 | pub struct KnownTypeNames; 10 | 11 | impl<'a> Visitor<'a> for KnownTypeNames { 12 | fn enter_fragment_definition( 13 | &mut self, 14 | ctx: &mut VisitorContext<'a>, 15 | _name: &'a Name, 16 | fragment_definition: &'a Positioned<FragmentDefinition>, 17 | ) { 18 | let TypeCondition { on: name } = &fragment_definition.node.type_condition.node; 19 | validate_type(ctx, &name.node, fragment_definition.pos); 20 | } 21 | 22 | fn enter_variable_definition( 23 | &mut self, 24 | ctx: &mut VisitorContext<'a>, 25 | variable_definition: &'a Positioned<VariableDefinition>, 26 | ) { 27 | validate_type( 28 | ctx, 29 | variable_definition.node.var_type.node.concrete_typename(), 30 | variable_definition.pos, 31 | ); 32 | } 33 | 34 | fn enter_inline_fragment( 35 | &mut self, 36 | ctx: &mut VisitorContext<'a>, 37 | inline_fragment: &'a Positioned<InlineFragment>, 38 | ) { 39 | if let Some(TypeCondition { on: name }) = inline_fragment 40 | .node 41 | .type_condition 42 | .as_ref() 43 | .map(|c| &c.node) 44 | { 45 | validate_type(ctx, &name.node, inline_fragment.pos); 46 | } 47 | } 48 | } 49 | 50 | fn validate_type(ctx: &mut VisitorContext<'_>, type_name: &str, pos: Pos) { 51 | if !ctx.schema.types.contains_key(type_name) { 52 | ctx.report_error(vec![pos], format!(r#"Unknown type "{}""#, type_name)); 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use super::*; 59 | 60 | pub fn factory() -> KnownTypeNames { 61 | KnownTypeNames::default() 62 | } 63 | 64 | #[test] 65 | fn known_type_names_are_valid() { 66 | expect_passes_rule!( 67 | factory, 68 | r#" 69 | query Foo($var: String, $required: [String!]!) { 70 | user(id: 4) { 71 | pets { ... on Pet { name }, ...PetFields, ... { name } } 72 | } 73 | } 74 | fragment PetFields on Pet { 75 | name 76 | } 77 | "#, 78 | ); 79 | } 80 | 81 | #[test] 82 | fn unknown_type_names_are_invalid() { 83 | expect_fails_rule!( 84 | factory, 85 | r#" 86 | query Foo($var: JumbledUpLetters) { 87 | user(id: 4) { 88 | name 89 | pets { ... on Badger { name }, ...PetFields } 90 | } 91 | } 92 | fragment PetFields on Peettt { 93 | name 94 | } 95 | "#, 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /crates/validation/src/rules/mod.rs: -------------------------------------------------------------------------------- 1 | mod arguments_of_correct_type; 2 | mod default_values_of_correct_type; 3 | mod fields_on_correct_type; 4 | mod fragments_on_composite_types; 5 | mod known_argument_names; 6 | mod known_directives; 7 | mod known_fragment_names; 8 | mod known_type_names; 9 | mod no_fragment_cycles; 10 | mod no_undefined_variables; 11 | mod no_unused_fragments; 12 | mod no_unused_variables; 13 | mod overlapping_fields_can_be_merged; 14 | mod possible_fragment_spreads; 15 | mod provided_non_null_arguments; 16 | mod scalar_leafs; 17 | mod unique_argument_names; 18 | mod unique_variable_names; 19 | mod variables_are_input_types; 20 | mod variables_in_allowed_position; 21 | 22 | pub use arguments_of_correct_type::ArgumentsOfCorrectType; 23 | pub use default_values_of_correct_type::DefaultValuesOfCorrectType; 24 | pub use fields_on_correct_type::FieldsOnCorrectType; 25 | pub use fragments_on_composite_types::FragmentsOnCompositeTypes; 26 | pub use known_argument_names::KnownArgumentNames; 27 | pub use known_directives::KnownDirectives; 28 | pub use known_fragment_names::KnownFragmentNames; 29 | pub use known_type_names::KnownTypeNames; 30 | pub use no_fragment_cycles::NoFragmentCycles; 31 | pub use no_undefined_variables::NoUndefinedVariables; 32 | pub use no_unused_fragments::NoUnusedFragments; 33 | pub use no_unused_variables::NoUnusedVariables; 34 | pub use overlapping_fields_can_be_merged::OverlappingFieldsCanBeMerged; 35 | pub use possible_fragment_spreads::PossibleFragmentSpreads; 36 | pub use provided_non_null_arguments::ProvidedNonNullArguments; 37 | pub use scalar_leafs::ScalarLeafs; 38 | pub use unique_argument_names::UniqueArgumentNames; 39 | pub use unique_variable_names::UniqueVariableNames; 40 | pub use variables_are_input_types::VariablesAreInputTypes; 41 | pub use variables_in_allowed_position::VariableInAllowedPosition; 42 | -------------------------------------------------------------------------------- /crates/validation/src/rules/no_unused_fragments.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | 3 | use parser::types::{ExecutableDocument, FragmentDefinition, FragmentSpread, OperationDefinition}; 4 | use parser::{Pos, Positioned}; 5 | use value::Name; 6 | 7 | use crate::utils::Scope; 8 | use crate::{Visitor, VisitorContext}; 9 | 10 | #[derive(Default)] 11 | pub struct NoUnusedFragments<'a> { 12 | spreads: HashMap<Scope<'a>, Vec<&'a str>>, 13 | defined_fragments: HashSet<(&'a str, Pos)>, 14 | current_scope: Option<Scope<'a>>, 15 | } 16 | 17 | impl<'a> NoUnusedFragments<'a> { 18 | fn find_reachable_fragments(&self, from: &Scope<'a>, result: &mut HashSet<&'a str>) { 19 | if let Scope::Fragment(name) = *from { 20 | if result.contains(name) { 21 | return; 22 | } else { 23 | result.insert(name); 24 | } 25 | } 26 | 27 | if let Some(spreads) = self.spreads.get(from) { 28 | for spread in spreads { 29 | self.find_reachable_fragments(&Scope::Fragment(spread), result) 30 | } 31 | } 32 | } 33 | } 34 | 35 | impl<'a> Visitor<'a> for NoUnusedFragments<'a> { 36 | fn exit_document(&mut self, ctx: &mut VisitorContext<'a>, doc: &'a ExecutableDocument) { 37 | let mut reachable = HashSet::new(); 38 | 39 | for (name, _) in doc.operations.iter() { 40 | self.find_reachable_fragments( 41 | &Scope::Operation(name.map(Name::as_str)), 42 | &mut reachable, 43 | ); 44 | } 45 | 46 | for (fragment_name, pos) in &self.defined_fragments { 47 | if !reachable.contains(fragment_name) { 48 | ctx.report_error( 49 | vec![*pos], 50 | format!(r#"Fragment "{}" is never used"#, fragment_name), 51 | ); 52 | } 53 | } 54 | } 55 | 56 | fn enter_operation_definition( 57 | &mut self, 58 | _ctx: &mut VisitorContext<'a>, 59 | name: Option<&'a Name>, 60 | _operation_definition: &'a Positioned<OperationDefinition>, 61 | ) { 62 | self.current_scope = Some(Scope::Operation(name.map(Name::as_str))); 63 | } 64 | 65 | fn enter_fragment_definition( 66 | &mut self, 67 | _ctx: &mut VisitorContext<'a>, 68 | name: &'a Name, 69 | fragment_definition: &'a Positioned<FragmentDefinition>, 70 | ) { 71 | self.current_scope = Some(Scope::Fragment(name)); 72 | self.defined_fragments 73 | .insert((name, fragment_definition.pos)); 74 | } 75 | 76 | fn enter_fragment_spread( 77 | &mut self, 78 | _ctx: &mut VisitorContext<'a>, 79 | fragment_spread: &'a Positioned<FragmentSpread>, 80 | ) { 81 | if let Some(ref scope) = self.current_scope { 82 | self.spreads 83 | .entry(*scope) 84 | .or_insert_with(Vec::new) 85 | .push(&fragment_spread.node.fragment_name.node); 86 | } 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use super::*; 93 | 94 | pub fn factory<'a>() -> NoUnusedFragments<'a> { 95 | NoUnusedFragments::default() 96 | } 97 | 98 | #[test] 99 | fn all_fragment_names_are_used() { 100 | expect_passes_rule!( 101 | factory, 102 | r#" 103 | { 104 | human(id: 4) { 105 | ...HumanFields1 106 | ... on Human { 107 | ...HumanFields2 108 | } 109 | } 110 | } 111 | fragment HumanFields1 on Human { 112 | name 113 | ...HumanFields3 114 | } 115 | fragment HumanFields2 on Human { 116 | name 117 | } 118 | fragment HumanFields3 on Human { 119 | name 120 | } 121 | "#, 122 | ); 123 | } 124 | 125 | #[test] 126 | fn all_fragment_names_are_used_by_multiple_operations() { 127 | expect_passes_rule!( 128 | factory, 129 | r#" 130 | query Foo { 131 | human(id: 4) { 132 | ...HumanFields1 133 | } 134 | } 135 | query Bar { 136 | human(id: 4) { 137 | ...HumanFields2 138 | } 139 | } 140 | fragment HumanFields1 on Human { 141 | name 142 | ...HumanFields3 143 | } 144 | fragment HumanFields2 on Human { 145 | name 146 | } 147 | fragment HumanFields3 on Human { 148 | name 149 | } 150 | "#, 151 | ); 152 | } 153 | 154 | #[test] 155 | fn contains_unknown_fragments() { 156 | expect_fails_rule!( 157 | factory, 158 | r#" 159 | query Foo { 160 | human(id: 4) { 161 | ...HumanFields1 162 | } 163 | } 164 | query Bar { 165 | human(id: 4) { 166 | ...HumanFields2 167 | } 168 | } 169 | fragment HumanFields1 on Human { 170 | name 171 | ...HumanFields3 172 | } 173 | fragment HumanFields2 on Human { 174 | name 175 | } 176 | fragment HumanFields3 on Human { 177 | name 178 | } 179 | fragment Unused1 on Human { 180 | name 181 | } 182 | fragment Unused2 on Human { 183 | name 184 | } 185 | "#, 186 | ); 187 | } 188 | 189 | #[test] 190 | fn contains_unknown_fragments_with_ref_cycle() { 191 | expect_fails_rule!( 192 | factory, 193 | r#" 194 | query Foo { 195 | human(id: 4) { 196 | ...HumanFields1 197 | } 198 | } 199 | query Bar { 200 | human(id: 4) { 201 | ...HumanFields2 202 | } 203 | } 204 | fragment HumanFields1 on Human { 205 | name 206 | ...HumanFields3 207 | } 208 | fragment HumanFields2 on Human { 209 | name 210 | } 211 | fragment HumanFields3 on Human { 212 | name 213 | } 214 | fragment Unused1 on Human { 215 | name 216 | ...Unused2 217 | } 218 | fragment Unused2 on Human { 219 | name 220 | ...Unused1 221 | } 222 | "#, 223 | ); 224 | } 225 | 226 | #[test] 227 | fn contains_unknown_and_undef_fragments() { 228 | expect_fails_rule!( 229 | factory, 230 | r#" 231 | query Foo { 232 | human(id: 4) { 233 | ...bar 234 | } 235 | } 236 | fragment foo on Human { 237 | name 238 | } 239 | "#, 240 | ); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /crates/validation/src/rules/overlapping_fields_can_be_merged.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use parser::types::{Field, Selection, SelectionSet}; 4 | use parser::Positioned; 5 | 6 | use crate::{Visitor, VisitorContext}; 7 | 8 | #[derive(Default)] 9 | pub struct OverlappingFieldsCanBeMerged; 10 | 11 | impl<'a> Visitor<'a> for OverlappingFieldsCanBeMerged { 12 | fn enter_selection_set( 13 | &mut self, 14 | ctx: &mut VisitorContext<'a>, 15 | selection_set: &'a Positioned<SelectionSet>, 16 | ) { 17 | let mut find_conflicts = FindConflicts { 18 | outputs: Default::default(), 19 | ctx, 20 | }; 21 | find_conflicts.find(selection_set); 22 | } 23 | } 24 | 25 | struct FindConflicts<'a, 'ctx> { 26 | outputs: HashMap<&'a str, &'a Positioned<Field>>, 27 | ctx: &'a mut VisitorContext<'ctx>, 28 | } 29 | 30 | impl<'a, 'ctx> FindConflicts<'a, 'ctx> { 31 | pub fn find(&mut self, selection_set: &'a Positioned<SelectionSet>) { 32 | for selection in &selection_set.node.items { 33 | match &selection.node { 34 | Selection::Field(field) => { 35 | let output_name = field 36 | .node 37 | .alias 38 | .as_ref() 39 | .map(|name| &name.node) 40 | .unwrap_or_else(|| &field.node.name.node); 41 | self.add_output(&output_name, field); 42 | } 43 | Selection::InlineFragment(inline_fragment) => { 44 | self.find(&inline_fragment.node.selection_set); 45 | } 46 | Selection::FragmentSpread(fragment_spread) => { 47 | if let Some(fragment) = 48 | self.ctx.fragment(&fragment_spread.node.fragment_name.node) 49 | { 50 | self.find(&fragment.node.selection_set); 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | fn add_output(&mut self, name: &'a str, field: &'a Positioned<Field>) { 58 | if let Some(prev_field) = self.outputs.get(name) { 59 | if prev_field.node.name.node != field.node.name.node { 60 | self.ctx.report_error( 61 | vec![prev_field.pos, field.pos], 62 | format!("Fields \"{}\" conflict because \"{}\" and \"{}\" are different fields. Use different aliases on the fields to fetch both if this was intentional.", 63 | name, prev_field.node.name.node, field.node.name.node)); 64 | } 65 | 66 | // check arguments 67 | if prev_field.node.arguments.len() != field.node.arguments.len() { 68 | self.ctx.report_error( 69 | vec![prev_field.pos, field.pos], 70 | format!("Fields \"{}\" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional.", name)); 71 | } 72 | 73 | for (name, value) in &prev_field.node.arguments { 74 | match field.node.get_argument(&name.node) { 75 | Some(other_value) if value == other_value => {} 76 | _=> self.ctx.report_error( 77 | vec![prev_field.pos, field.pos], 78 | format!("Fields \"{}\" conflict because they have differing arguments. Use different aliases on the fields to fetch both if this was intentional.", name)), 79 | } 80 | } 81 | } else { 82 | self.outputs.insert(name, field); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /crates/validation/src/rules/provided_non_null_arguments.rs: -------------------------------------------------------------------------------- 1 | use parser::types::{Directive, Field}; 2 | use parser::Positioned; 3 | 4 | use crate::{Visitor, VisitorContext}; 5 | 6 | #[derive(Default)] 7 | pub struct ProvidedNonNullArguments; 8 | 9 | impl<'a> Visitor<'a> for ProvidedNonNullArguments { 10 | fn enter_directive( 11 | &mut self, 12 | ctx: &mut VisitorContext<'a>, 13 | directive: &'a Positioned<Directive>, 14 | ) { 15 | if let Some(schema_directive) = ctx.schema.directives.get(directive.node.name.node.as_str()) 16 | { 17 | for arg in schema_directive.arguments.values() { 18 | if !arg.ty.nullable 19 | && arg.default_value.is_none() 20 | && directive 21 | .node 22 | .arguments 23 | .iter() 24 | .find(|(name, _)| name.node == arg.name) 25 | .is_none() 26 | { 27 | ctx.report_error(vec![directive.pos], 28 | format!( 29 | "Directive \"@{}\" argument \"{}\" of type \"{}\" is required but not provided", 30 | directive.node.name, arg.name, arg.ty 31 | )); 32 | } 33 | } 34 | } 35 | } 36 | 37 | fn enter_field(&mut self, ctx: &mut VisitorContext<'a>, field: &'a Positioned<Field>) { 38 | if let Some(parent_type) = ctx.parent_type() { 39 | if let Some(schema_field) = parent_type.field_by_name(&field.node.name.node) { 40 | for arg in schema_field.arguments.values() { 41 | if !arg.ty.nullable 42 | && arg.default_value.is_none() 43 | && field 44 | .node 45 | .arguments 46 | .iter() 47 | .find(|(name, _)| name.node == arg.name) 48 | .is_none() 49 | { 50 | ctx.report_error(vec![field.pos], 51 | format!( 52 | r#"Field "{}" argument "{}" of type "{}" is required but not provided"#, 53 | field.node.name, arg.name, parent_type.name 54 | )); 55 | } 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use super::*; 65 | 66 | pub fn factory() -> ProvidedNonNullArguments { 67 | ProvidedNonNullArguments 68 | } 69 | 70 | #[test] 71 | fn ignores_unknown_arguments() { 72 | expect_passes_rule!( 73 | factory, 74 | r#" 75 | { 76 | dog { 77 | isHousetrained(unknownArgument: true) 78 | } 79 | } 80 | "#, 81 | ); 82 | } 83 | 84 | #[test] 85 | fn arg_on_optional_arg() { 86 | expect_passes_rule!( 87 | factory, 88 | r#" 89 | { 90 | dog { 91 | isHousetrained(atOtherHomes: true) 92 | } 93 | } 94 | "#, 95 | ); 96 | } 97 | 98 | #[test] 99 | fn no_arg_on_optional_arg() { 100 | expect_passes_rule!( 101 | factory, 102 | r#" 103 | { 104 | dog { 105 | isHousetrained 106 | } 107 | } 108 | "#, 109 | ); 110 | } 111 | 112 | #[test] 113 | fn multiple_args() { 114 | expect_passes_rule!( 115 | factory, 116 | r#" 117 | { 118 | complicatedArgs { 119 | multipleReqs(req1: 1, req2: 2) 120 | } 121 | } 122 | "#, 123 | ); 124 | } 125 | 126 | #[test] 127 | fn multiple_args_reverse_order() { 128 | expect_passes_rule!( 129 | factory, 130 | r#" 131 | { 132 | complicatedArgs { 133 | multipleReqs(req2: 2, req1: 1) 134 | } 135 | } 136 | "#, 137 | ); 138 | } 139 | 140 | #[test] 141 | fn no_args_on_multiple_optional() { 142 | expect_passes_rule!( 143 | factory, 144 | r#" 145 | { 146 | complicatedArgs { 147 | multipleOpts 148 | } 149 | } 150 | "#, 151 | ); 152 | } 153 | 154 | #[test] 155 | fn one_arg_on_multiple_optional() { 156 | expect_passes_rule!( 157 | factory, 158 | r#" 159 | { 160 | complicatedArgs { 161 | multipleOpts(opt1: 1) 162 | } 163 | } 164 | "#, 165 | ); 166 | } 167 | 168 | #[test] 169 | fn second_arg_on_multiple_optional() { 170 | expect_passes_rule!( 171 | factory, 172 | r#" 173 | { 174 | complicatedArgs { 175 | multipleOpts(opt2: 1) 176 | } 177 | } 178 | "#, 179 | ); 180 | } 181 | 182 | #[test] 183 | fn muliple_reqs_on_mixed_list() { 184 | expect_passes_rule!( 185 | factory, 186 | r#" 187 | { 188 | complicatedArgs { 189 | multipleOptAndReq(req1: 3, req2: 4) 190 | } 191 | } 192 | "#, 193 | ); 194 | } 195 | 196 | #[test] 197 | fn multiple_reqs_and_one_opt_on_mixed_list() { 198 | expect_passes_rule!( 199 | factory, 200 | r#" 201 | { 202 | complicatedArgs { 203 | multipleOptAndReq(req1: 3, req2: 4, opt1: 5) 204 | } 205 | } 206 | "#, 207 | ); 208 | } 209 | 210 | #[test] 211 | fn all_reqs_on_opts_on_mixed_list() { 212 | expect_passes_rule!( 213 | factory, 214 | r#" 215 | { 216 | complicatedArgs { 217 | multipleOptAndReq(req1: 3, req2: 4, opt1: 5, opt2: 6) 218 | } 219 | } 220 | "#, 221 | ); 222 | } 223 | 224 | #[test] 225 | fn missing_one_non_nullable_argument() { 226 | expect_fails_rule!( 227 | factory, 228 | r#" 229 | { 230 | complicatedArgs { 231 | multipleReqs(req2: 2) 232 | } 233 | } 234 | "#, 235 | ); 236 | } 237 | 238 | #[test] 239 | fn missing_multiple_non_nullable_arguments() { 240 | expect_fails_rule!( 241 | factory, 242 | r#" 243 | { 244 | complicatedArgs { 245 | multipleReqs 246 | } 247 | } 248 | "#, 249 | ); 250 | } 251 | 252 | #[test] 253 | fn incorrect_value_and_missing_argument() { 254 | expect_fails_rule!( 255 | factory, 256 | r#" 257 | { 258 | complicatedArgs { 259 | multipleReqs(req1: "one") 260 | } 261 | } 262 | "#, 263 | ); 264 | } 265 | 266 | #[test] 267 | fn ignores_unknown_directives() { 268 | expect_passes_rule!( 269 | factory, 270 | r#" 271 | { 272 | dog @unknown 273 | } 274 | "#, 275 | ); 276 | } 277 | 278 | #[test] 279 | fn with_directives_of_valid_types() { 280 | expect_passes_rule!( 281 | factory, 282 | r#" 283 | { 284 | dog @include(if: true) { 285 | name 286 | } 287 | human @skip(if: false) { 288 | name 289 | } 290 | } 291 | "#, 292 | ); 293 | } 294 | 295 | #[test] 296 | fn with_directive_with_missing_types() { 297 | expect_fails_rule!( 298 | factory, 299 | r#" 300 | { 301 | dog @include { 302 | name @skip 303 | } 304 | } 305 | "#, 306 | ); 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /crates/validation/src/rules/scalar_leafs.rs: -------------------------------------------------------------------------------- 1 | use parser::types::Field; 2 | use parser::Positioned; 3 | 4 | use crate::{Visitor, VisitorContext}; 5 | 6 | #[derive(Default)] 7 | pub struct ScalarLeafs; 8 | 9 | impl<'a> Visitor<'a> for ScalarLeafs { 10 | fn enter_field(&mut self, ctx: &mut VisitorContext<'a>, field: &'a Positioned<Field>) { 11 | if let Some(ty) = ctx.parent_type() { 12 | if let Some(schema_field) = ty.field_by_name(&field.node.name.node) { 13 | if let Some(ty) = ctx.schema.concrete_type_by_name(&schema_field.ty) { 14 | if ty.is_leaf() && !field.node.selection_set.node.items.is_empty() { 15 | ctx.report_error(vec![field.pos], format!( 16 | "Field \"{}\" must not have a selection since type \"{}\" has no subfields", 17 | field.node.name, ty.name 18 | )) 19 | } else if !ty.is_leaf() && field.node.selection_set.node.items.is_empty() { 20 | ctx.report_error( 21 | vec![field.pos], 22 | format!( 23 | "Field \"{}\" of type \"{}\" must have a selection of subfields", 24 | field.node.name, ty.name 25 | ), 26 | ) 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | #[cfg(test)] 35 | mod tests { 36 | use super::*; 37 | 38 | pub fn factory() -> ScalarLeafs { 39 | ScalarLeafs 40 | } 41 | 42 | #[test] 43 | fn valid_scalar_selection() { 44 | expect_passes_rule!( 45 | factory, 46 | r#" 47 | fragment scalarSelection on Dog { 48 | barks 49 | } 50 | { __typename } 51 | "#, 52 | ); 53 | } 54 | 55 | #[test] 56 | fn object_type_missing_selection() { 57 | expect_fails_rule!( 58 | factory, 59 | r#" 60 | query directQueryOnObjectWithoutSubFields { 61 | human 62 | } 63 | "#, 64 | ); 65 | } 66 | 67 | #[test] 68 | fn interface_type_missing_selection() { 69 | expect_fails_rule!( 70 | factory, 71 | r#" 72 | { 73 | human { pets } 74 | } 75 | "#, 76 | ); 77 | } 78 | 79 | #[test] 80 | fn valid_scalar_selection_with_args() { 81 | expect_passes_rule!( 82 | factory, 83 | r#" 84 | fragment scalarSelectionWithArgs on Dog { 85 | doesKnowCommand(dogCommand: SIT) 86 | } 87 | { __typename } 88 | "#, 89 | ); 90 | } 91 | 92 | #[test] 93 | fn scalar_selection_not_allowed_on_boolean() { 94 | expect_fails_rule!( 95 | factory, 96 | r#" 97 | fragment scalarSelectionsNotAllowedOnBoolean on Dog { 98 | barks { sinceWhen } 99 | } 100 | { __typename } 101 | "#, 102 | ); 103 | } 104 | 105 | #[test] 106 | fn scalar_selection_not_allowed_on_enum() { 107 | expect_fails_rule!( 108 | factory, 109 | r#" 110 | fragment scalarSelectionsNotAllowedOnEnum on Cat { 111 | furColor { inHexdec } 112 | } 113 | { __typename } 114 | "#, 115 | ); 116 | } 117 | 118 | #[test] 119 | fn scalar_selection_not_allowed_with_args() { 120 | expect_fails_rule!( 121 | factory, 122 | r#" 123 | fragment scalarSelectionsNotAllowedWithArgs on Dog { 124 | doesKnowCommand(dogCommand: SIT) { sinceWhen } 125 | } 126 | { __typename } 127 | "#, 128 | ); 129 | } 130 | 131 | #[test] 132 | fn scalar_selection_not_allowed_with_directives() { 133 | expect_fails_rule!( 134 | factory, 135 | r#" 136 | fragment scalarSelectionsNotAllowedWithDirectives on Dog { 137 | name @include(if: true) { isAlsoHumanName } 138 | } 139 | { __typename } 140 | "#, 141 | ); 142 | } 143 | 144 | #[test] 145 | fn scalar_selection_not_allowed_with_directives_and_args() { 146 | expect_fails_rule!( 147 | factory, 148 | r#" 149 | fragment scalarSelectionsNotAllowedWithDirectivesAndArgs on Dog { 150 | doesKnowCommand(dogCommand: SIT) @include(if: true) { sinceWhen } 151 | } 152 | { __typename } 153 | "#, 154 | ); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /crates/validation/src/rules/unique_argument_names.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use parser::types::{Directive, Field}; 4 | use parser::Positioned; 5 | use value::{Name, Value}; 6 | 7 | use crate::{Visitor, VisitorContext}; 8 | 9 | #[derive(Default)] 10 | pub struct UniqueArgumentNames<'a> { 11 | names: HashSet<&'a str>, 12 | } 13 | 14 | impl<'a> Visitor<'a> for UniqueArgumentNames<'a> { 15 | fn enter_directive( 16 | &mut self, 17 | _ctx: &mut VisitorContext<'a>, 18 | _directive: &'a Positioned<Directive>, 19 | ) { 20 | self.names.clear(); 21 | } 22 | 23 | fn enter_argument( 24 | &mut self, 25 | ctx: &mut VisitorContext<'a>, 26 | name: &'a Positioned<Name>, 27 | _value: &'a Positioned<Value>, 28 | ) { 29 | if !self.names.insert(name.node.as_str()) { 30 | ctx.report_error( 31 | vec![name.pos], 32 | format!("There can only be one argument named \"{}\"", name), 33 | ) 34 | } 35 | } 36 | 37 | fn enter_field(&mut self, _ctx: &mut VisitorContext<'a>, _field: &'a Positioned<Field>) { 38 | self.names.clear(); 39 | } 40 | } 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use super::*; 45 | 46 | pub fn factory<'a>() -> UniqueArgumentNames<'a> { 47 | UniqueArgumentNames::default() 48 | } 49 | 50 | #[test] 51 | fn no_arguments_on_field() { 52 | expect_passes_rule!( 53 | factory, 54 | r#" 55 | { 56 | field 57 | } 58 | "#, 59 | ); 60 | } 61 | 62 | #[test] 63 | fn no_arguments_on_directive() { 64 | expect_passes_rule!( 65 | factory, 66 | r#" 67 | { 68 | dog @directive 69 | } 70 | "#, 71 | ); 72 | } 73 | 74 | #[test] 75 | fn argument_on_field() { 76 | expect_passes_rule!( 77 | factory, 78 | r#" 79 | { 80 | field(arg: "value") 81 | } 82 | "#, 83 | ); 84 | } 85 | 86 | #[test] 87 | fn argument_on_directive() { 88 | expect_passes_rule!( 89 | factory, 90 | r#" 91 | { 92 | dog @directive(arg: "value") 93 | } 94 | "#, 95 | ); 96 | } 97 | 98 | #[test] 99 | fn same_argument_on_two_fields() { 100 | expect_passes_rule!( 101 | factory, 102 | r#" 103 | { 104 | one: field(arg: "value") 105 | two: field(arg: "value") 106 | } 107 | "#, 108 | ); 109 | } 110 | 111 | #[test] 112 | fn same_argument_on_field_and_directive() { 113 | expect_passes_rule!( 114 | factory, 115 | r#" 116 | { 117 | field(arg: "value") @directive(arg: "value") 118 | } 119 | "#, 120 | ); 121 | } 122 | 123 | #[test] 124 | fn same_argument_on_two_directives() { 125 | expect_passes_rule!( 126 | factory, 127 | r#" 128 | { 129 | field @directive1(arg: "value") @directive2(arg: "value") 130 | } 131 | "#, 132 | ); 133 | } 134 | 135 | #[test] 136 | fn multiple_field_arguments() { 137 | expect_passes_rule!( 138 | factory, 139 | r#" 140 | { 141 | field(arg1: "value", arg2: "value", arg3: "value") 142 | } 143 | "#, 144 | ); 145 | } 146 | 147 | #[test] 148 | fn multiple_directive_arguments() { 149 | expect_passes_rule!( 150 | factory, 151 | r#" 152 | { 153 | field @directive(arg1: "value", arg2: "value", arg3: "value") 154 | } 155 | "#, 156 | ); 157 | } 158 | 159 | #[test] 160 | fn duplicate_field_arguments() { 161 | expect_fails_rule!( 162 | factory, 163 | r#" 164 | { 165 | field(arg1: "value", arg1: "value") 166 | } 167 | "#, 168 | ); 169 | } 170 | 171 | #[test] 172 | fn many_duplicate_field_arguments() { 173 | expect_fails_rule!( 174 | factory, 175 | r#" 176 | { 177 | field(arg1: "value", arg1: "value", arg1: "value") 178 | } 179 | "#, 180 | ); 181 | } 182 | 183 | #[test] 184 | fn duplicate_directive_arguments() { 185 | expect_fails_rule!( 186 | factory, 187 | r#" 188 | { 189 | field @directive(arg1: "value", arg1: "value") 190 | } 191 | "#, 192 | ); 193 | } 194 | 195 | #[test] 196 | fn many_duplicate_directive_arguments() { 197 | expect_fails_rule!( 198 | factory, 199 | r#" 200 | { 201 | field @directive(arg1: "value", arg1: "value", arg1: "value") 202 | } 203 | "#, 204 | ); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /crates/validation/src/rules/unique_variable_names.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use parser::types::{OperationDefinition, VariableDefinition}; 4 | use parser::Positioned; 5 | use value::Name; 6 | 7 | use crate::{Visitor, VisitorContext}; 8 | 9 | #[derive(Default)] 10 | pub struct UniqueVariableNames<'a> { 11 | names: HashSet<&'a str>, 12 | } 13 | 14 | impl<'a> Visitor<'a> for UniqueVariableNames<'a> { 15 | fn enter_operation_definition( 16 | &mut self, 17 | _ctx: &mut VisitorContext<'a>, 18 | _name: Option<&'a Name>, 19 | _operation_definition: &'a Positioned<OperationDefinition>, 20 | ) { 21 | self.names.clear(); 22 | } 23 | 24 | fn enter_variable_definition( 25 | &mut self, 26 | ctx: &mut VisitorContext<'a>, 27 | variable_definition: &'a Positioned<VariableDefinition>, 28 | ) { 29 | if !self.names.insert(&variable_definition.node.name.node) { 30 | ctx.report_error( 31 | vec![variable_definition.pos], 32 | format!( 33 | "There can only be one variable named \"${}\"", 34 | variable_definition.node.name.node 35 | ), 36 | ); 37 | } 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | 45 | pub fn factory<'a>() -> UniqueVariableNames<'a> { 46 | UniqueVariableNames::default() 47 | } 48 | 49 | #[test] 50 | fn unique_variable_names() { 51 | expect_passes_rule!( 52 | factory, 53 | r#" 54 | query A($x: Int, $y: String) { __typename } 55 | query B($x: String, $y: Int) { __typename } 56 | "#, 57 | ); 58 | } 59 | 60 | #[test] 61 | fn duplicate_variable_names() { 62 | expect_fails_rule!( 63 | factory, 64 | r#" 65 | query A($x: Int, $x: Int, $x: String) { __typename } 66 | query B($x: String, $x: Int) { __typename } 67 | query C($x: Int, $x: Int) { __typename } 68 | "#, 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /crates/validation/src/rules/variables_are_input_types.rs: -------------------------------------------------------------------------------- 1 | use parser::types::VariableDefinition; 2 | use parser::Positioned; 3 | 4 | use crate::{Visitor, VisitorContext}; 5 | 6 | #[derive(Default)] 7 | pub struct VariablesAreInputTypes; 8 | 9 | impl<'a> Visitor<'a> for VariablesAreInputTypes { 10 | fn enter_variable_definition( 11 | &mut self, 12 | ctx: &mut VisitorContext<'a>, 13 | variable_definition: &'a Positioned<VariableDefinition>, 14 | ) { 15 | if let Some(ty) = ctx 16 | .schema 17 | .concrete_type_by_name(&variable_definition.node.var_type.node) 18 | { 19 | if !ty.is_input() { 20 | ctx.report_error( 21 | vec![variable_definition.pos], 22 | format!( 23 | "Variable \"{}\" cannot be of non-input type \"{}\"", 24 | variable_definition.node.name.node, ty.name 25 | ), 26 | ); 27 | } 28 | } 29 | } 30 | } 31 | 32 | #[cfg(test)] 33 | mod tests { 34 | use super::*; 35 | 36 | pub fn factory() -> VariablesAreInputTypes { 37 | VariablesAreInputTypes 38 | } 39 | 40 | #[test] 41 | fn input_types_are_valid() { 42 | expect_passes_rule!( 43 | factory, 44 | r#" 45 | query Foo($a: String, $b: [Boolean!]!, $c: ComplexInput) { 46 | field(a: $a, b: $b, c: $c) 47 | } 48 | "#, 49 | ); 50 | } 51 | 52 | #[test] 53 | fn output_types_are_invalid() { 54 | expect_fails_rule!( 55 | factory, 56 | r#" 57 | query Foo($a: Dog, $b: [[CatOrDog!]]!, $c: Pet) { 58 | field(a: $a, b: $b, c: $c) 59 | } 60 | "#, 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/validation/src/suggestion.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt::Write; 3 | 4 | fn levenshtein_distance(s1: &str, s2: &str) -> usize { 5 | let mut column: Vec<_> = (0..=s1.len()).collect(); 6 | for (x, rx) in s2.bytes().enumerate() { 7 | column[0] = x + 1; 8 | let mut lastdiag = x; 9 | for (y, ry) in s1.bytes().enumerate() { 10 | let olddiag = column[y + 1]; 11 | if rx != ry { 12 | lastdiag += 1; 13 | } 14 | column[y + 1] = (column[y + 1] + 1).min((column[y] + 1).min(lastdiag)); 15 | lastdiag = olddiag; 16 | } 17 | } 18 | column[s1.len()] 19 | } 20 | 21 | pub fn make_suggestion<'a, I>(prefix: &str, options: I, input: &str) -> Option<String> 22 | where 23 | I: Iterator<Item = &'a str>, 24 | { 25 | let mut selected = Vec::new(); 26 | let mut distances = HashMap::new(); 27 | 28 | for opt in options { 29 | let distance = levenshtein_distance(input, opt); 30 | let threshold = (input.len() / 2).max((opt.len() / 2).max(1)); 31 | if distance < threshold { 32 | selected.push(opt); 33 | distances.insert(opt, distance); 34 | } 35 | } 36 | 37 | if selected.is_empty() { 38 | return None; 39 | } 40 | selected.sort_by(|a, b| distances[a].cmp(&distances[b])); 41 | 42 | let mut suggestion = 43 | String::with_capacity(prefix.len() + selected.iter().map(|s| s.len() + 5).sum::<usize>()); 44 | suggestion.push_str(prefix); 45 | suggestion.push(' '); 46 | 47 | for (i, s) in selected.iter().enumerate() { 48 | if i != 0 { 49 | suggestion.push_str(", "); 50 | } 51 | write!(suggestion, "\"{}\"", s).unwrap(); 52 | } 53 | 54 | suggestion.push('?'); 55 | 56 | Some(suggestion) 57 | } 58 | -------------------------------------------------------------------------------- /crates/validation/src/test_harness.graphql: -------------------------------------------------------------------------------- 1 | input TestInput { 2 | id: Int! 3 | name: Int! 4 | } 5 | 6 | enum DogCommand { 7 | SIT 8 | HEEL 9 | DOWN 10 | } 11 | 12 | type Dog implements Pet & Being & Canine { 13 | name(surname: Boolean): String 14 | nickname: String 15 | barkVolume: Int 16 | barks: Boolean 17 | doesKnowCommand(dogCommand: DogCommand): Boolean 18 | isHousetrained(atOtherHomes: Boolean = true): Boolean 19 | isAtLocation(x: Int, y: Int): Boolean 20 | } 21 | 22 | enum FurColor { 23 | BROWN 24 | BLACK 25 | TAN 26 | SPOTTED 27 | } 28 | 29 | type Cat implements Pet & Being { 30 | name(surname: Boolean): String 31 | nickname: String 32 | meows: Boolean 33 | meowVolume: Int 34 | furColor: FurColor 35 | } 36 | 37 | union CatOrDog = Cat | Dog 38 | 39 | type Human implements Being & Intelligent { 40 | name(surname: Boolean): String 41 | pets: [Pet] 42 | relatives: [Human] 43 | iq: Int 44 | } 45 | 46 | type Alien implements Being & Intelligent { 47 | name(surname: Boolean): String 48 | iq: Int 49 | numEyes: Int 50 | } 51 | 52 | union DogOrHuman = Dog | Human 53 | 54 | union HumanOrAlien = Human | Alien 55 | 56 | interface Being { 57 | name(surname: Boolean): String 58 | } 59 | 60 | interface Pet { 61 | name(surname: Boolean): String 62 | } 63 | 64 | interface Canine { 65 | name(surname: Boolean): String 66 | } 67 | 68 | interface Intelligent { 69 | iq: Int 70 | } 71 | 72 | input ComplexInput { 73 | requiredField: Boolean! 74 | intField: Int 75 | stringField: String 76 | booleanField: Boolean 77 | stringListField: [String] 78 | } 79 | 80 | type ComplicatedArgs { 81 | intArgField(intArg: Int): String 82 | nonNullIntArgField(nonNullIntArg: Int!): String 83 | stringArgField(stringArg: String): String 84 | booleanArgField(booleanArg: Boolean): String 85 | enumArgField(enumArg: FurColor): String 86 | floatArgField(floatArg: Float): String 87 | idArgField(idArg: ID): String 88 | stringListArgField(stringListArg: [String]): String 89 | complexArgField(complexArg: ComplexInput): String 90 | multipleReqs(req1: Int!, req2: Int!): String 91 | multipleOpts(opt1: Int! = 0, opt2: Int! = 0): String 92 | multipleOptAndReq(req1: Int!, req2: Int!, opt1: Int! = 0, opt2: Int! = 0): String 93 | } 94 | 95 | type Query { 96 | human(id: ID): Human 97 | alien: Alien 98 | dog: Dog 99 | cat: Cat 100 | pet: Pet 101 | being: Being 102 | intelligent: Intelligent 103 | catOrDog: CatOrDog 104 | dogOrHuman: DogOrHuman 105 | humanOrAlien: HumanOrAlien 106 | complicatedArgs: ComplicatedArgs 107 | } 108 | 109 | type Mutation { 110 | testInput(input: TestInput! = {id: 0, name: 0}): Int 111 | } 112 | 113 | schema { 114 | query: Query 115 | mutation: Mutation 116 | } 117 | -------------------------------------------------------------------------------- /crates/validation/src/test_harness.rs: -------------------------------------------------------------------------------- 1 | use graphgate_schema::ComposedSchema; 2 | use once_cell::sync::Lazy; 3 | use parser::types::ExecutableDocument; 4 | use value::Variables; 5 | 6 | use crate::visitor::{visit, Visitor, VisitorContext}; 7 | use crate::RuleError; 8 | 9 | static SCHEMA: Lazy<ComposedSchema> = 10 | Lazy::new(|| ComposedSchema::parse(include_str!("test_harness.graphql")).unwrap()); 11 | 12 | pub fn validate<'a, V, F>( 13 | doc: &'a ExecutableDocument, 14 | variables: &'a Variables, 15 | factory: F, 16 | ) -> Result<(), Vec<RuleError>> 17 | where 18 | V: Visitor<'a> + 'a, 19 | F: Fn() -> V, 20 | { 21 | let mut ctx = VisitorContext::new(&*SCHEMA, doc, variables); 22 | let mut visitor = factory(); 23 | visit(&mut visitor, &mut ctx, doc); 24 | if ctx.errors.is_empty() { 25 | Ok(()) 26 | } else { 27 | Err(ctx.errors) 28 | } 29 | } 30 | 31 | pub fn expect_passes_rule_<'a, V, F>( 32 | doc: &'a ExecutableDocument, 33 | variables: &'a Variables, 34 | factory: F, 35 | ) where 36 | V: Visitor<'a> + 'a, 37 | F: Fn() -> V, 38 | { 39 | if let Err(errors) = validate(doc, variables, factory) { 40 | for err in errors { 41 | if let Some(position) = err.locations.first() { 42 | print!("[{}:{}] ", position.line, position.column); 43 | } 44 | println!("{}", err.message); 45 | } 46 | panic!("Expected rule to pass, but errors found"); 47 | } 48 | } 49 | 50 | macro_rules! expect_passes_rule { 51 | ($factory:expr, $query_source:literal $(,)?) => { 52 | let variables = value::Variables::default(); 53 | let doc = parser::parse_query($query_source).expect("Parse error"); 54 | crate::test_harness::expect_passes_rule_(&doc, &variables, $factory); 55 | }; 56 | } 57 | 58 | pub fn expect_fails_rule_<'a, V, F>( 59 | doc: &'a ExecutableDocument, 60 | variables: &'a Variables, 61 | factory: F, 62 | ) where 63 | V: Visitor<'a> + 'a, 64 | F: Fn() -> V, 65 | { 66 | if validate(doc, variables, factory).is_ok() { 67 | panic!("Expected rule to fail, but no errors were found"); 68 | } 69 | } 70 | 71 | macro_rules! expect_fails_rule { 72 | ($factory:expr, $query_source:literal $(,)?) => { 73 | let variables = value::Variables::default(); 74 | let doc = parser::parse_query($query_source).expect("Parse error"); 75 | crate::test_harness::expect_fails_rule_(&doc, &variables, $factory); 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /examples/accounts.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use async_graphql::{EmptyMutation, Object, Schema, SimpleObject, Subscription, ID}; 4 | use async_graphql_warp::{graphql, graphql_subscription}; 5 | use futures_util::stream::Stream; 6 | use tokio::time::Duration; 7 | use warp::{Filter, Reply}; 8 | 9 | #[derive(SimpleObject)] 10 | struct User { 11 | id: ID, 12 | username: String, 13 | } 14 | 15 | struct Query; 16 | 17 | #[Object(extends)] 18 | impl Query { 19 | /// Get the current user. 20 | async fn me(&self) -> User { 21 | User { 22 | id: "1234".into(), 23 | username: "Me".to_string(), 24 | } 25 | } 26 | 27 | #[graphql(entity)] 28 | async fn find_user_by_id(&self, id: ID) -> User { 29 | let username = if id == "1234" { 30 | "Me".to_string() 31 | } else { 32 | format!("User {:?}", id) 33 | }; 34 | User { id, username } 35 | } 36 | } 37 | 38 | struct Subscription; 39 | 40 | #[Subscription(extends)] 41 | impl Subscription { 42 | async fn users(&self) -> impl Stream<Item = User> { 43 | async_stream::stream! { 44 | loop { 45 | tokio::time::sleep(Duration::from_secs(fastrand::u64((1..3)))).await; 46 | yield User { id: "1234".into(), username: "Me".to_string() }; 47 | } 48 | } 49 | } 50 | } 51 | 52 | #[tokio::main] 53 | async fn main() { 54 | let schema = Schema::build(Query, EmptyMutation, Subscription) 55 | .extension(async_graphql::extensions::ApolloTracing) 56 | .enable_subscription_in_federation() 57 | .finish(); 58 | 59 | let routes = graphql(schema.clone()) 60 | .and(warp::post()) 61 | .and_then( 62 | |(schema, request): ( 63 | Schema<Query, EmptyMutation, Subscription>, 64 | async_graphql::Request, 65 | )| async move { 66 | Ok::<_, Infallible>( 67 | warp::reply::json(&schema.execute(request).await).into_response(), 68 | ) 69 | }, 70 | ) 71 | .or(graphql_subscription(schema)); 72 | 73 | warp::serve(routes).run(([0, 0, 0, 0], 8001)).await; 74 | } 75 | -------------------------------------------------------------------------------- /examples/builtin_scalar_bug/bug.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use async_graphql::{EmptyMutation, EmptySubscription, Object, Schema, SimpleObject}; 4 | use async_graphql_warp::{graphql, graphql_subscription}; 5 | use serde::{Deserialize, Serialize}; 6 | use warp::{Filter, Reply}; 7 | 8 | // Run the service: 9 | // ``` 10 | // $ cargo run --example builtin_scalar_bug 11 | // ``` 12 | // 13 | // Run the gateway: 14 | // ``` 15 | // $ cargo run -- ./examples/builtin_scalar_bug/config.toml 16 | // ``` 17 | // 18 | // Query the service directly: 19 | // ``` 20 | // $ curl -s 'http://localhost:8001' --data-binary '{"query":"{ builtinScalar customScalar }\n"}' | jq .data 21 | // { 22 | // "builtinScalar": "Hi, I'm builtin", 23 | // "customScalar": "Hi, I'm custom" 24 | // } 25 | // ``` 26 | // 27 | // Run the same query through the gateway: 28 | // ``` 29 | // $ curl -s 'http://localhost:8000' --data-binary '{"query":"{ builtinScalar customScalar }\n"}' | jq .data 30 | // { 31 | // "builtinScalar": null, 32 | // "customScalar": "Hi, I'm custom" 33 | // } 34 | // ``` 35 | // 36 | // :( 37 | 38 | #[derive(Serialize, Deserialize)] 39 | struct CustomString(String); 40 | async_graphql::scalar!(CustomString); 41 | 42 | struct Query; 43 | 44 | #[Object(extends)] 45 | impl Query { 46 | async fn builtin_scalar(&self) -> String { 47 | "Hi, I'm builtin".into() 48 | } 49 | async fn custom_scalar(&self) -> CustomString { 50 | CustomString("Hi, I'm custom".into()) 51 | } 52 | 53 | #[graphql(entity)] // just so we get _service 54 | async fn find_me(&self, constant: String) -> BuiltinScalarBug { 55 | BuiltinScalarBug { constant } 56 | } 57 | } 58 | 59 | #[derive(SimpleObject)] 60 | struct BuiltinScalarBug { 61 | constant: String, 62 | } 63 | 64 | #[tokio::main] 65 | async fn main() { 66 | let schema = Schema::build(Query, EmptyMutation, EmptySubscription).finish(); 67 | 68 | let routes = graphql(schema.clone()) 69 | .and(warp::post()) 70 | .and_then( 71 | |(schema, request): ( 72 | Schema<Query, EmptyMutation, EmptySubscription>, 73 | async_graphql::Request, 74 | )| async move { 75 | Ok::<_, Infallible>( 76 | warp::reply::json(&schema.execute(request).await).into_response(), 77 | ) 78 | }, 79 | ) 80 | .or(graphql_subscription(schema)); 81 | 82 | warp::serve(routes).run(([0, 0, 0, 0], 8001)).await; 83 | } 84 | -------------------------------------------------------------------------------- /examples/builtin_scalar_bug/config.toml: -------------------------------------------------------------------------------- 1 | bind = "0.0.0.0:8000" 2 | 3 | [[services]] 4 | name = "builtin_scalar_bug" 5 | addr = "127.0.0.1:8001" 6 | -------------------------------------------------------------------------------- /examples/helm/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /examples/helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: graphgate-example 3 | description: A Helm chart for Kubernetes 4 | type: application 5 | version: 0.1.0 6 | -------------------------------------------------------------------------------- /examples/helm/config.toml: -------------------------------------------------------------------------------- 1 | bind = "0.0.0.0:8000" 2 | 3 | [jaeger] 4 | agent_endpoint = "jaeger-agent:6831" 5 | -------------------------------------------------------------------------------- /examples/helm/templates/examples.yaml: -------------------------------------------------------------------------------- 1 | {{- range .Values.services }} 2 | --- 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: {{ .name }} 7 | spec: 8 | replicas: {{ $.Values.example_replicas }} 9 | selector: 10 | matchLabels: 11 | app: {{ .name }} 12 | strategy: 13 | type: RollingUpdate 14 | template: 15 | metadata: 16 | labels: 17 | app: {{ .name }} 18 | spec: 19 | containers: 20 | - image: scott829/graphgate-examples:latest 21 | imagePullPolicy: Always 22 | name: {{ .name }} 23 | ports: 24 | - containerPort: 8000 25 | name: http 26 | command: 27 | - {{ .name }} 28 | --- 29 | apiVersion: v1 30 | kind: Service 31 | metadata: 32 | name: {{ .name }} 33 | labels: 34 | graphgate.org/service: {{ .name }} 35 | spec: 36 | ports: 37 | - name: graphql 38 | port: 8000 39 | targetPort: {{ .port }} 40 | selector: 41 | app: {{ .name }} 42 | {{- end }} 43 | -------------------------------------------------------------------------------- /examples/helm/templates/graphgate.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: graphgate 6 | automountServiceAccountToken: true 7 | --- 8 | kind: ClusterRole 9 | apiVersion: rbac.authorization.k8s.io/v1 10 | metadata: 11 | name: graphql-services-view 12 | rules: 13 | - apiGroups: [""] 14 | resources: ["services"] 15 | verbs: ["get", "list"] 16 | --- 17 | kind: ClusterRoleBinding 18 | apiVersion: rbac.authorization.k8s.io/v1 19 | metadata: 20 | name: graphgate-binding 21 | subjects: 22 | - kind: ServiceAccount 23 | name: graphgate 24 | namespace: {{ .Release.Namespace }} 25 | roleRef: 26 | kind: ClusterRole 27 | name: graphql-services-view 28 | apiGroup: rbac.authorization.k8s.io 29 | --- 30 | apiVersion: v1 31 | kind: ConfigMap 32 | metadata: 33 | name: graphgate-config 34 | data: 35 | config.toml: {{ .Files.Get "config.toml" | quote }} 36 | --- 37 | apiVersion: apps/v1 38 | kind: Deployment 39 | metadata: 40 | name: graphgate 41 | spec: 42 | replicas: {{ $.Values.gateway_replicas }} 43 | selector: 44 | matchLabels: 45 | app: graphgate 46 | strategy: 47 | type: RollingUpdate 48 | template: 49 | metadata: 50 | labels: 51 | app: graphgate 52 | spec: 53 | serviceAccountName: graphgate 54 | volumes: 55 | - name: config-volume 56 | configMap: 57 | name: graphgate-config 58 | containers: 59 | - image: scott829/graphgate:latest 60 | imagePullPolicy: Always 61 | name: graphgate 62 | volumeMounts: 63 | - mountPath: /config 64 | name: config-volume 65 | args: 66 | - /config/config.toml 67 | readinessProbe: 68 | httpGet: 69 | path: /health 70 | port: {{ .Values.port }} 71 | initialDelaySeconds: 5 72 | periodSeconds: 5 73 | ports: 74 | - containerPort: 8000 75 | name: http 76 | env: 77 | - name: RUST_LOG 78 | value: graphgate=debug 79 | --- 80 | apiVersion: v1 81 | kind: Service 82 | metadata: 83 | name: graphgate 84 | spec: 85 | ports: 86 | - name: http 87 | port: 8000 88 | nodePort: 31000 89 | targetPort: {{ .Values.port }} 90 | selector: 91 | app: graphgate 92 | type: NodePort 93 | -------------------------------------------------------------------------------- /examples/helm/templates/jaeger.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: jaeger 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: jaeger 11 | strategy: 12 | type: Recreate 13 | template: 14 | metadata: 15 | labels: 16 | app: jaeger 17 | spec: 18 | containers: 19 | - image: jaegertracing/all-in-one:1.22.0 20 | name: jaeger 21 | env: 22 | - name: JAEGER_AGENT_PORT 23 | value: "6831" 24 | --- 25 | apiVersion: v1 26 | kind: Service 27 | metadata: 28 | name: jaeger-ui 29 | spec: 30 | ports: 31 | - name: http 32 | port: 16686 33 | nodePort: 31001 34 | targetPort: 16686 35 | selector: 36 | app: jaeger 37 | type: NodePort 38 | --- 39 | apiVersion: v1 40 | kind: Service 41 | metadata: 42 | name: jaeger-agent 43 | spec: 44 | ports: 45 | - name: udp 46 | port: 6831 47 | protocol: UDP 48 | targetPort: 6831 49 | selector: 50 | app: jaeger 51 | clusterIP: None 52 | -------------------------------------------------------------------------------- /examples/helm/values.yaml: -------------------------------------------------------------------------------- 1 | port: 8000 2 | 3 | gateway_replicas: 3 4 | example_replicas: 3 5 | 6 | services: 7 | - name: accounts 8 | port: 8001 9 | - name: products 10 | port: 8002 11 | - name: reviews 12 | port: 8003 13 | -------------------------------------------------------------------------------- /examples/products.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use async_graphql::{Context, EmptyMutation, Object, Schema, SimpleObject, Subscription}; 4 | use async_graphql_warp::{graphql, graphql_subscription}; 5 | use futures_util::stream::Stream; 6 | use tokio::time::Duration; 7 | use warp::{Filter, Reply}; 8 | 9 | #[derive(SimpleObject, Clone)] 10 | struct Product { 11 | upc: String, 12 | name: String, 13 | price: i32, 14 | } 15 | 16 | struct Query; 17 | 18 | #[Object(extends)] 19 | impl Query { 20 | async fn top_products<'a>(&self, ctx: &'a Context<'_>) -> &'a Vec<Product> { 21 | ctx.data_unchecked::<Vec<Product>>() 22 | } 23 | 24 | #[graphql(entity)] 25 | async fn find_product_by_upc<'a>(&self, ctx: &Context<'a>, upc: String) -> Option<&'a Product> { 26 | let hats = ctx.data_unchecked::<Vec<Product>>(); 27 | hats.iter().find(|product| product.upc == upc) 28 | } 29 | } 30 | 31 | struct Subscription; 32 | 33 | #[Subscription(extends)] 34 | impl Subscription { 35 | async fn products(&self) -> impl Stream<Item = Product> { 36 | async_stream::stream! { 37 | loop { 38 | tokio::time::sleep(Duration::from_secs(fastrand::u64((5..10)))).await; 39 | yield Product { 40 | upc: "top-1".to_string(), 41 | name: "Trilby".to_string(), 42 | price: 11, 43 | }; 44 | } 45 | } 46 | } 47 | } 48 | 49 | #[tokio::main] 50 | async fn main() { 51 | let hats = vec![ 52 | Product { 53 | upc: "top-1".to_string(), 54 | name: "Trilby".to_string(), 55 | price: 11, 56 | }, 57 | Product { 58 | upc: "top-2".to_string(), 59 | name: "Fedora".to_string(), 60 | price: 22, 61 | }, 62 | Product { 63 | upc: "top-3".to_string(), 64 | name: "Boater".to_string(), 65 | price: 33, 66 | }, 67 | ]; 68 | 69 | let schema = Schema::build(Query, EmptyMutation, Subscription) 70 | .extension(async_graphql::extensions::ApolloTracing) 71 | .enable_subscription_in_federation() 72 | .data(hats) 73 | .finish(); 74 | 75 | let routes = graphql(schema.clone()) 76 | .and(warp::post()) 77 | .and_then( 78 | |(schema, request): ( 79 | Schema<Query, EmptyMutation, Subscription>, 80 | async_graphql::Request, 81 | )| async move { 82 | Ok::<_, Infallible>( 83 | warp::reply::json(&schema.execute(request).await).into_response(), 84 | ) 85 | }, 86 | ) 87 | .or(graphql_subscription(schema)); 88 | 89 | warp::serve(routes).run(([0, 0, 0, 0], 8002)).await; 90 | } 91 | -------------------------------------------------------------------------------- /examples/reviews.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use async_graphql::{Context, EmptyMutation, Object, Schema, SimpleObject, Subscription, ID}; 4 | use async_graphql_warp::{graphql, graphql_subscription}; 5 | use futures_util::stream::Stream; 6 | use tokio::time::Duration; 7 | use warp::{Filter, Reply}; 8 | 9 | struct User { 10 | id: ID, 11 | } 12 | 13 | #[Object(extends)] 14 | impl User { 15 | #[graphql(external)] 16 | async fn id(&self) -> &ID { 17 | &self.id 18 | } 19 | 20 | async fn reviews<'a>(&self, ctx: &'a Context<'_>) -> Vec<&'a Review> { 21 | let reviews = ctx.data_unchecked::<Vec<Review>>(); 22 | reviews 23 | .iter() 24 | .filter(|review| review.author.id == self.id) 25 | .collect() 26 | } 27 | } 28 | 29 | struct Product { 30 | upc: String, 31 | } 32 | 33 | #[Object(extends)] 34 | impl Product { 35 | #[graphql(external)] 36 | async fn upc(&self) -> &String { 37 | &self.upc 38 | } 39 | 40 | async fn reviews<'a>(&self, ctx: &'a Context<'_>) -> Vec<&'a Review> { 41 | let reviews = ctx.data_unchecked::<Vec<Review>>(); 42 | reviews 43 | .iter() 44 | .filter(|review| review.product.upc == self.upc) 45 | .collect() 46 | } 47 | 48 | async fn error(&self) -> Result<i32, &str> { 49 | return Err("custom error"); 50 | } 51 | } 52 | 53 | #[derive(SimpleObject)] 54 | struct Review { 55 | body: String, 56 | author: User, 57 | product: Product, 58 | } 59 | 60 | struct Query; 61 | 62 | #[Object] 63 | impl Query { 64 | #[graphql(entity)] 65 | async fn find_user_by_id(&self, id: ID) -> User { 66 | User { id } 67 | } 68 | 69 | #[graphql(entity)] 70 | async fn find_product_by_upc(&self, upc: String) -> Product { 71 | Product { upc } 72 | } 73 | } 74 | 75 | struct Subscription; 76 | 77 | #[Subscription(extends)] 78 | impl Subscription { 79 | async fn reviews(&self) -> impl Stream<Item = Review> { 80 | async_stream::stream! { 81 | loop { 82 | tokio::time::sleep(Duration::from_secs(fastrand::u64((5..10)))).await; 83 | yield Review { 84 | body: "A highly effective form of birth control.".into(), 85 | author: User { id: "1234".into() }, 86 | product: Product { 87 | upc: "top-1".to_string(), 88 | }, 89 | }; 90 | } 91 | } 92 | } 93 | } 94 | 95 | #[tokio::main] 96 | async fn main() { 97 | let reviews = vec![ 98 | Review { 99 | body: "A highly effective form of birth control.".into(), 100 | author: User { id: "1234".into() }, 101 | product: Product { 102 | upc: "top-1".to_string(), 103 | }, 104 | }, 105 | Review { 106 | body: "Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.".into(), 107 | author: User { id: "1234".into() }, 108 | product: Product { 109 | upc: "top-1".to_string(), 110 | }, 111 | }, 112 | Review { 113 | body: "This is the last straw. Hat you will wear. 11/10".into(), 114 | author: User { id: "7777".into() }, 115 | product: Product { 116 | upc: "top-1".to_string(), 117 | }, 118 | }, 119 | ]; 120 | 121 | let schema = Schema::build(Query, EmptyMutation, Subscription) 122 | .extension(async_graphql::extensions::ApolloTracing) 123 | .enable_subscription_in_federation() 124 | .data(reviews) 125 | .finish(); 126 | 127 | let routes = graphql(schema.clone()) 128 | .and(warp::post()) 129 | .and_then( 130 | |(schema, request): ( 131 | Schema<Query, EmptyMutation, Subscription>, 132 | async_graphql::Request, 133 | )| async move { 134 | Ok::<_, Infallible>( 135 | warp::reply::json(&schema.execute(request).await).into_response(), 136 | ) 137 | }, 138 | ) 139 | .or(graphql_subscription(schema)); 140 | 141 | warp::serve(routes).run(([0, 0, 0, 0], 8003)).await; 142 | } 143 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use graphgate_handler::{ServiceRoute, ServiceRouteTable}; 2 | use serde::Deserialize; 3 | 4 | #[derive(Debug, Deserialize)] 5 | pub struct Config { 6 | #[serde(default = "default_bind")] 7 | pub bind: String, 8 | 9 | #[serde(default)] 10 | pub gateway_name: String, 11 | 12 | #[serde(default)] 13 | pub services: Vec<ServiceConfig>, 14 | 15 | #[serde(default)] 16 | pub forward_headers: Vec<String>, 17 | 18 | #[serde(default)] 19 | pub receive_headers: Vec<String>, 20 | 21 | pub jaeger: Option<JaegerConfig>, 22 | 23 | pub cors: Option<CorsConfig>, 24 | } 25 | 26 | #[derive(Debug, Deserialize, Clone)] 27 | pub struct ServiceConfig { 28 | pub name: String, 29 | pub addr: String, 30 | #[serde(default)] 31 | pub tls: bool, 32 | pub query_path: Option<String>, 33 | pub subscribe_path: Option<String>, 34 | pub introspection_path: Option<String>, 35 | pub websocket_path: Option<String>, 36 | } 37 | 38 | impl ServiceConfig { 39 | // websocket path should default to query path unless set 40 | fn default_or_set_websocket_path(&self) -> Option<String> { 41 | if self.websocket_path.is_some() { 42 | self.websocket_path.clone() 43 | } else { 44 | self.query_path.clone() 45 | } 46 | } 47 | } 48 | 49 | #[derive(Debug, Deserialize)] 50 | pub struct CorsConfig { 51 | pub allow_any_origin: Option<bool>, 52 | pub allow_methods: Option<Vec<String>>, 53 | pub allow_credentials: Option<bool>, 54 | pub allow_headers: Option<Vec<String>>, 55 | pub allow_origins: Option<Vec<String>>, 56 | } 57 | 58 | #[derive(Debug, Deserialize)] 59 | pub struct JaegerConfig { 60 | pub agent_endpoint: String, 61 | 62 | #[serde(default = "default_jaeger_service_name")] 63 | pub service_name: String, 64 | } 65 | 66 | impl Config { 67 | pub fn create_route_table(&self) -> ServiceRouteTable { 68 | let mut route_table = ServiceRouteTable::default(); 69 | for service in &self.services { 70 | route_table.insert( 71 | service.name.clone(), 72 | ServiceRoute { 73 | addr: service.addr.clone(), 74 | tls: service.tls, 75 | query_path: service.query_path.clone(), 76 | subscribe_path: service.subscribe_path.clone(), 77 | introspection_path: service.introspection_path.clone(), 78 | websocket_path: service.default_or_set_websocket_path(), 79 | }, 80 | ); 81 | } 82 | route_table 83 | } 84 | } 85 | 86 | fn default_bind() -> String { 87 | "127.0.0.1:8000".to_string() 88 | } 89 | 90 | fn default_jaeger_service_name() -> String { 91 | "graphgate".to_string() 92 | } 93 | -------------------------------------------------------------------------------- /src/k8s.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use graphgate_handler::{ServiceRoute, ServiceRouteTable}; 3 | use k8s_openapi::api::core::v1::Service; 4 | use kube::api::{ListParams, ObjectMeta}; 5 | use kube::{Api, Client}; 6 | 7 | const NAMESPACE_PATH: &str = "/var/run/secrets/kubernetes.io/serviceaccount/namespace"; 8 | const LABEL_GRAPHQL_SERVICE: &str = "graphgate.org/service"; 9 | const LABEL_GRAPHQL_GATEWAY: &str = "graphgate.org/gateway"; 10 | const ANNOTATIONS_TLS: &str = "graphgate.org/tls"; 11 | const ANNOTATIONS_QUERY_PATH: &str = "graphgate.org/queryPath"; 12 | const ANNOTATIONS_SUBSCRIBE_PATH: &str = "graphgate.org/subscribePath"; 13 | const ANNOTATIONS_INTROSPECTION_PATH: &str = "graphgate.org/introspectionPath"; 14 | const ANNOTATIONS_WEBSOCKET_PATH: &str = "graphgate.org/websocketPath"; 15 | 16 | fn get_label_value<'a>(meta: &'a ObjectMeta, name: &str) -> Option<&'a str> { 17 | meta.labels 18 | .iter() 19 | .flatten() 20 | .find(|(key, _)| key.as_str() == name) 21 | .map(|(_, value)| value.as_str()) 22 | } 23 | 24 | fn get_annotation_value<'a>(meta: &'a ObjectMeta, name: &str) -> Option<&'a str> { 25 | meta.annotations 26 | .iter() 27 | .flatten() 28 | .find(|(key, _)| key.as_str() == name) 29 | .map(|(_, value)| value.as_str()) 30 | } 31 | 32 | fn get_gateway_or_default(gateway_name: &str) -> String { 33 | match gateway_name.len() > 0 { 34 | true => { 35 | tracing::trace!( 36 | "Found gateway name: {}. Looking for gateway labels instead.", 37 | gateway_name 38 | ); 39 | format!("{}={}", LABEL_GRAPHQL_GATEWAY, gateway_name) 40 | } 41 | false => LABEL_GRAPHQL_SERVICE.to_string(), 42 | } 43 | } 44 | 45 | pub async fn find_graphql_services(gateway_name: &str) -> Result<ServiceRouteTable> { 46 | tracing::trace!("Find GraphQL services."); 47 | let client = Client::try_default() 48 | .await 49 | .context("Failed to create kube client.")?; 50 | 51 | let namespace = 52 | std::fs::read_to_string(NAMESPACE_PATH).unwrap_or_else(|_| "default".to_string()); 53 | tracing::trace!(namespace = %namespace, "Get current namespace."); 54 | 55 | let mut route_table = ServiceRouteTable::default(); 56 | let services_api: Api<Service> = Api::namespaced(client, &namespace); 57 | 58 | tracing::trace!("List all services."); 59 | let services = services_api 60 | .list(&ListParams::default().labels(get_gateway_or_default(gateway_name).as_str())) 61 | .await 62 | .context("Failed to call list services api")?; 63 | 64 | for service in &services { 65 | if let Some((host, service_name)) = service 66 | .metadata 67 | .name 68 | .as_deref() 69 | .zip(get_label_value(&service.metadata, LABEL_GRAPHQL_SERVICE)) 70 | { 71 | for service_port in service 72 | .spec 73 | .iter() 74 | .map(|spec| spec.ports.iter()) 75 | .flatten() 76 | .flatten() 77 | { 78 | let tls = get_annotation_value(&service.metadata, ANNOTATIONS_TLS).is_some(); 79 | let query_path = get_annotation_value(&service.metadata, ANNOTATIONS_QUERY_PATH); 80 | let subscribe_path = 81 | get_annotation_value(&service.metadata, ANNOTATIONS_SUBSCRIBE_PATH); 82 | let introspection_path = 83 | get_annotation_value(&service.metadata, ANNOTATIONS_INTROSPECTION_PATH); 84 | let websocket_path = 85 | get_annotation_value(&service.metadata, ANNOTATIONS_WEBSOCKET_PATH); 86 | route_table.insert( 87 | service_name.to_string(), 88 | ServiceRoute { 89 | addr: format!("{}:{}", host, service_port.port), 90 | tls, 91 | query_path: query_path.map(ToString::to_string), 92 | subscribe_path: subscribe_path.map(ToString::to_string), 93 | introspection_path: introspection_path.map(ToString::to_string), 94 | websocket_path: websocket_path.map(ToString::to_string), 95 | }, 96 | ); 97 | } 98 | } 99 | } 100 | 101 | Ok(route_table) 102 | } 103 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | mod config; 4 | mod k8s; 5 | mod options; 6 | 7 | use std::net::SocketAddr; 8 | use std::sync::Arc; 9 | 10 | use anyhow::{Context, Result}; 11 | use futures_util::FutureExt; 12 | use graphgate_handler::handler::HandlerConfig; 13 | use graphgate_handler::{handler, SharedRouteTable}; 14 | use opentelemetry::global; 15 | use opentelemetry::global::GlobalTracerProvider; 16 | use opentelemetry::trace::noop::NoopTracerProvider; 17 | use opentelemetry_prometheus::PrometheusExporter; 18 | use prometheus::{Encoder, TextEncoder}; 19 | use structopt::StructOpt; 20 | use tokio::signal; 21 | use tokio::time::Duration; 22 | use tracing_subscriber::layer::SubscriberExt; 23 | use tracing_subscriber::util::SubscriberInitExt; 24 | use tracing_subscriber::{fmt, EnvFilter}; 25 | use warp::http::Response as HttpResponse; 26 | use warp::hyper::StatusCode; 27 | use warp::{Filter, Rejection, Reply}; 28 | 29 | use config::Config; 30 | use options::Options; 31 | 32 | // Use Jemalloc only for musl-64 bits platforms 33 | #[cfg(all(target_env = "musl", target_pointer_width = "64"))] 34 | #[global_allocator] 35 | static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; 36 | 37 | fn init_tracing() { 38 | tracing_subscriber::registry() 39 | .with(fmt::layer().compact().with_target(false)) 40 | .with( 41 | EnvFilter::try_from_default_env() 42 | .or_else(|_| EnvFilter::try_new("info")) 43 | .unwrap(), 44 | ) 45 | .init(); 46 | } 47 | 48 | async fn update_route_table_in_k8s(shared_route_table: SharedRouteTable, gateway_name: String) { 49 | let mut prev_route_table = None; 50 | loop { 51 | match k8s::find_graphql_services(&gateway_name).await { 52 | Ok(route_table) => { 53 | if Some(&route_table) != prev_route_table.as_ref() { 54 | tracing::info!(route_table = ?route_table, "Route table updated."); 55 | shared_route_table.set_route_table(route_table.clone()); 56 | prev_route_table = Some(route_table); 57 | } 58 | } 59 | Err(err) => { 60 | tracing::error!(error = %err, "Failed to find graphql services."); 61 | } 62 | } 63 | 64 | tokio::time::sleep(Duration::from_secs(30)).await; 65 | } 66 | } 67 | 68 | fn init_tracer(config: &Config) -> Result<GlobalTracerProvider> { 69 | let uninstall = match &config.jaeger { 70 | Some(config) => { 71 | tracing::info!( 72 | agent_endpoint = %config.agent_endpoint, 73 | service_name = %config.service_name, 74 | "Initialize Jaeger" 75 | ); 76 | let provider = opentelemetry_jaeger::new_pipeline() 77 | .with_agent_endpoint(&config.agent_endpoint) 78 | .with_service_name(&config.service_name) 79 | .build_batch(opentelemetry::runtime::Tokio) 80 | .context("Failed to initialize jaeger.")?; 81 | global::set_tracer_provider(provider) 82 | } 83 | None => { 84 | let provider = NoopTracerProvider::new(); 85 | global::set_tracer_provider(provider) 86 | } 87 | }; 88 | Ok(uninstall) 89 | } 90 | 91 | pub fn metrics( 92 | exporter: PrometheusExporter, 93 | ) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone { 94 | warp::path!("metrics").and(warp::get()).map({ 95 | move || { 96 | let mut buffer = Vec::new(); 97 | let encoder = TextEncoder::new(); 98 | let metric_families = exporter.registry().gather(); 99 | if let Err(err) = encoder.encode(&metric_families, &mut buffer) { 100 | return HttpResponse::builder() 101 | .status(StatusCode::INTERNAL_SERVER_ERROR) 102 | .body(err.to_string().into_bytes()) 103 | .unwrap(); 104 | } 105 | HttpResponse::builder() 106 | .status(StatusCode::OK) 107 | .body(buffer) 108 | .unwrap() 109 | } 110 | }) 111 | } 112 | 113 | #[tokio::main] 114 | async fn main() -> Result<()> { 115 | let options: Options = Options::from_args(); 116 | init_tracing(); 117 | 118 | let config = toml::from_str::<Config>( 119 | &std::fs::read_to_string(&options.config) 120 | .with_context(|| format!("Failed to load config file '{}'.", options.config))?, 121 | ) 122 | .with_context(|| format!("Failed to parse config file '{}'.", options.config))?; 123 | let _uninstall = init_tracer(&config)?; 124 | let exporter = opentelemetry_prometheus::exporter().init(); 125 | 126 | let mut shared_route_table = SharedRouteTable::default(); 127 | if !config.services.is_empty() { 128 | tracing::info!("Route table in the configuration file."); 129 | shared_route_table.set_route_table(config.create_route_table()); 130 | shared_route_table.set_receive_headers(config.receive_headers); 131 | } else if std::env::var("KUBERNETES_SERVICE_HOST").is_ok() { 132 | tracing::info!("Route table within the current namespace in Kubernetes cluster."); 133 | shared_route_table.set_receive_headers(config.receive_headers); 134 | tokio::spawn(update_route_table_in_k8s( 135 | shared_route_table.clone(), 136 | config.gateway_name.clone(), 137 | )); 138 | } else { 139 | tracing::info!("Route table is empty."); 140 | return Ok(()); 141 | } 142 | 143 | let handler_config = HandlerConfig { 144 | shared_route_table, 145 | forward_headers: Arc::new(config.forward_headers), 146 | }; 147 | 148 | let cors = if let Some(cors_config) = config.cors { 149 | let warp_cors = warp::cors(); 150 | 151 | let origins_vec = cors_config.allow_origins.unwrap_or_default(); 152 | 153 | let origins: Vec<&str> = origins_vec.iter().map(|s| s as &str).collect(); 154 | 155 | let headers_vec = cors_config.allow_headers.unwrap_or_default(); 156 | 157 | let headers: Vec<&str> = headers_vec.iter().map(|s| s as &str).collect(); 158 | 159 | let allow_credentials = cors_config.allow_credentials.unwrap_or(false); 160 | 161 | let allow_methods_vec = cors_config.allow_methods.unwrap_or_default(); 162 | 163 | let methods: Vec<&str> = allow_methods_vec.iter().map(|s| s as &str).collect(); 164 | 165 | let cors_setup = warp_cors 166 | .allow_headers(headers) 167 | .allow_origins(origins) 168 | .allow_methods(methods) 169 | .allow_credentials(allow_credentials); 170 | 171 | if let Some(true) = cors_config.allow_any_origin { 172 | Some(cors_setup.allow_any_origin()) 173 | } else { 174 | Some(cors_setup) 175 | } 176 | } else { 177 | None 178 | }; 179 | 180 | let graphql = warp::path::end().and( 181 | handler::graphql_request(handler_config.clone()) 182 | .or(handler::graphql_websocket(handler_config.clone())) 183 | .or(handler::graphql_playground()), 184 | ); 185 | let health = warp::path!("health").map(|| warp::reply::json(&"healthy")); 186 | 187 | let bind_addr: SocketAddr = config 188 | .bind 189 | .parse() 190 | .context(format!("Failed to parse bind addr '{}'", config.bind))?; 191 | if let Some(warp_cors) = cors { 192 | let routes = graphql.or(health).or(metrics(exporter)).with(warp_cors); 193 | let (addr, server) = warp::serve(routes) 194 | .bind_with_graceful_shutdown(bind_addr, signal::ctrl_c().map(|_| ())); 195 | tracing::info!(addr = %addr, "Listening"); 196 | server.await; 197 | tracing::info!("Server shutdown"); 198 | } else { 199 | let routes = graphql.or(health).or(metrics(exporter)); 200 | let (addr, server) = warp::serve(routes) 201 | .bind_with_graceful_shutdown(bind_addr, signal::ctrl_c().map(|_| ())); 202 | tracing::info!(addr = %addr, "Listening"); 203 | server.await; 204 | tracing::info!("Server shutdown"); 205 | } 206 | 207 | Ok(()) 208 | } 209 | -------------------------------------------------------------------------------- /src/options.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | 3 | #[derive(StructOpt)] 4 | pub struct Options { 5 | /// Path of the config file 6 | #[structopt(default_value = "config.toml")] 7 | pub config: String, 8 | } 9 | --------------------------------------------------------------------------------