Compare commits
190 commits
rework-cor
...
master
Author | SHA1 | Date | |
---|---|---|---|
203ff0a6e8 | |||
|
5bda80ed52 | ||
|
b75e1aa55a | ||
|
497c839f30 | ||
|
a925ccb193 | ||
|
dde5fcc9b5 | ||
|
3e01787926 | ||
|
ee3cb3cc4c | ||
|
33db9911e8 | ||
|
5f39b37b51 | ||
|
5fed6706b0 | ||
|
dc19d85fae | ||
|
b32004f022 | ||
|
f552654b6e | ||
|
1b2d2641b8 | ||
|
617f90159c | ||
|
ab85a28cf1 | ||
|
bb529cca97 | ||
|
eb1fac2425 | ||
|
257bc69dde | ||
|
86297a42c6 | ||
|
0e0e6a1ba1 | ||
|
0067603765 | ||
|
a3a5a19544 | ||
|
102710558a | ||
|
834b7373b9 | ||
|
41ee878663 | ||
|
8d1aa9c61c | ||
|
fb96fb1dd1 | ||
|
fb1531f90c | ||
|
eb2079b2bd | ||
|
43c5888156 | ||
|
c1a916dc87 | ||
|
681d242823 | ||
|
b3a7ffc6a2 | ||
|
2756260000 | ||
|
975da450fe | ||
|
6b82d1d80e | ||
|
256e6bc13c | ||
|
b1f4ecb1e0 | ||
|
7db44c0b28 | ||
|
5447cc63de | ||
|
caa9495f01 | ||
|
a4974e179b | ||
|
b6e82ddc9c | ||
|
3571fd995f | ||
|
a3a17bf6e5 | ||
|
9bdf8cf379 | ||
|
76b6b9023f | ||
|
3b4a187d61 | ||
|
29e30ea786 | ||
|
f96a3f4baa | ||
|
02e931b516 | ||
|
2682ee1418 | ||
|
6ffbb66719 | ||
|
6f9c5b1034 | ||
|
eac4e21c68 | ||
|
e758ea0e91 | ||
|
aade4c8c7c | ||
|
9c4ea327f9 | ||
|
c674548e64 | ||
|
26c199f2f3 | ||
|
03c3b65bbb | ||
|
6d65dffd28 | ||
|
aea0ef2549 | ||
|
6cf1bb30da | ||
|
26bbd6d912 | ||
|
27dbbcaef4 | ||
|
209e5e6d23 | ||
|
e3c1efd557 | ||
|
731a1b0445 | ||
|
9bb33525d4 | ||
|
8c74d9132d | ||
|
f0b63c4a63 | ||
|
5603e76dc0 | ||
|
63f6f8fae4 | ||
|
ec0d7d8097 | ||
|
86b5319315 | ||
|
847a38a4c6 | ||
|
ac6e60987f | ||
|
2ce7dae298 | ||
|
b9fe911d1f | ||
|
f33406f3ce | ||
|
d6297f493a | ||
|
f363b0d79e | ||
|
8a69e14c8b | ||
|
f213d5193c | ||
|
507a66a3bf | ||
|
a1bc775e00 | ||
|
c54137f7b4 | ||
|
e64287cfc8 | ||
|
b1b31ff8c0 | ||
|
b7be55fb4b | ||
|
31c468dd65 | ||
|
d75f940625 | ||
|
57e6c9813c | ||
|
df24749727 | ||
|
dc1f3e0c79 | ||
|
2a27a54160 | ||
|
5dec22ad7b | ||
|
9420f3c19e | ||
|
c0e1b3eae3 | ||
|
ace693585d | ||
|
7c03f922e0 | ||
|
f98bdf1a50 | ||
|
58ae682f91 | ||
|
bd8dc582a4 | ||
|
add0e23c5f | ||
|
4ef8cf7de9 | ||
|
904d9cdbb6 | ||
|
5b58b85a37 | ||
|
3645df27b6 | ||
|
813c0d1210 | ||
|
9f776fd0a1 | ||
|
a515d4b8b9 | ||
|
316b1887b2 | ||
|
2215cd0e0b | ||
|
d7103e2ef2 | ||
|
e75cf26995 | ||
|
d0fc062892 | ||
|
761710205a | ||
|
ba59c953a7 | ||
|
d0f50e73e0 | ||
|
8b530ebc12 | ||
|
da005fcb41 | ||
|
e76b961534 | ||
|
782eccba47 | ||
|
2598045e89 | ||
|
0fc95ddbff | ||
|
d11e351a49 | ||
|
828d059b1b | ||
|
aaf28e962d | ||
|
f3a1a0c65d | ||
|
57628de864 | ||
|
a433a278cf | ||
|
17a02fa766 | ||
|
801820a3b6 | ||
|
9849736582 | ||
|
a74ea9ccb6 | ||
|
cb26dee3b7 | ||
|
7e886e8fab | ||
|
f9d90277bf | ||
|
f172be5656 | ||
|
dadd318b85 | ||
|
279bbedc3c | ||
|
df9da4c776 | ||
|
20c76a6342 | ||
|
b375146466 | ||
|
421c8c9d58 | ||
|
108ccf2715 | ||
|
27430bf60c | ||
|
1474da53f6 | ||
|
a297b9b67e | ||
|
b172d51b6e | ||
|
f1ecde260c | ||
|
d9dfae6d59 | ||
|
d7dcb3b2e5 | ||
|
dc86fcf820 | ||
|
9ef6133a14 | ||
|
74710d8a59 | ||
|
aff49ae5ec | ||
|
a648dd7275 | ||
|
eb571368c3 | ||
|
42ad693fcb | ||
|
91064e9554 | ||
|
7e5c8819c2 | ||
|
b84621c3b3 | ||
|
e569476bcc | ||
|
42420c7ee1 | ||
|
5d53001994 | ||
|
14f4a31bb7 | ||
|
a61ed0eee5 | ||
|
5aadecedf6 | ||
|
150227f357 | ||
|
9e0b9692a9 | ||
|
9ae0c89537 | ||
|
3810666646 | ||
|
6aa6385437 | ||
|
f6aae142ea | ||
|
4737622052 | ||
|
d21cc68933 | ||
|
dbe1b181db | ||
|
b79b799824 | ||
|
3a68877de0 | ||
|
819e26aba1 | ||
|
e3a72edf70 | ||
|
e7cfa20254 | ||
|
f3df70693c | ||
|
3a2254b854 | ||
|
b8a23a1982 |
427 changed files with 21935 additions and 16511 deletions
2
.codespellrc
Normal file
2
.codespellrc
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[codespell]
|
||||||
|
ignore-words-list = crate
|
5
.github/dependabot.yml
vendored
5
.github/dependabot.yml
vendored
|
@ -9,3 +9,8 @@ updates:
|
||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
|
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /juniper/
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
|
306
.github/workflows/ci.yml
vendored
306
.github/workflows/ci.yml
vendored
|
@ -3,7 +3,7 @@ name: CI
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["master"]
|
branches: ["master"]
|
||||||
tags: ["juniper*@*"]
|
tags: ["juniper*"]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ["master"]
|
branches: ["master"]
|
||||||
|
|
||||||
|
@ -24,9 +24,10 @@ jobs:
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
needs:
|
needs:
|
||||||
- bench
|
- bench
|
||||||
|
- codespell
|
||||||
- clippy
|
- clippy
|
||||||
- example
|
|
||||||
- feature
|
- feature
|
||||||
|
- msrv
|
||||||
- release-check
|
- release-check
|
||||||
- rustfmt
|
- rustfmt
|
||||||
- test
|
- test
|
||||||
|
@ -46,22 +47,29 @@ jobs:
|
||||||
clippy:
|
clippy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
components: clippy
|
components: clippy
|
||||||
|
|
||||||
- run: make cargo.lint
|
- run: make cargo.lint
|
||||||
|
|
||||||
|
codespell:
|
||||||
|
name: codespell (Book)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: codespell-project/actions-codespell@v2
|
||||||
|
with:
|
||||||
|
path: book/
|
||||||
|
|
||||||
rustfmt:
|
rustfmt:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
|
||||||
toolchain: nightly
|
toolchain: nightly
|
||||||
components: rustfmt
|
components: rustfmt
|
||||||
|
|
||||||
|
@ -77,78 +85,53 @@ jobs:
|
||||||
bench:
|
bench:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
override: true
|
|
||||||
|
|
||||||
- run: cargo clippy -p juniper_benchmarks --benches -- -D warnings
|
- run: cargo clippy -p juniper_benchmarks --benches -- -D warnings
|
||||||
- run: cargo bench -p juniper_benchmarks
|
- run: cargo bench -p juniper_benchmarks
|
||||||
|
|
||||||
example:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
example:
|
|
||||||
- actix_subscriptions
|
|
||||||
- basic_subscriptions
|
|
||||||
- warp_async
|
|
||||||
- warp_subscriptions
|
|
||||||
os:
|
|
||||||
- ubuntu
|
|
||||||
- macOS
|
|
||||||
- windows
|
|
||||||
toolchain:
|
|
||||||
- stable
|
|
||||||
- beta
|
|
||||||
- nightly
|
|
||||||
runs-on: ${{ matrix.os }}-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
profile: minimal
|
|
||||||
toolchain: ${{ matrix.toolchain }}
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- run: cargo check -p example_${{ matrix.example }}
|
|
||||||
|
|
||||||
feature:
|
feature:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- { feature: <none>, crate: juniper }
|
- { feature: <none>, crate: juniper }
|
||||||
|
- { feature: anyhow, crate: juniper }
|
||||||
|
- { feature: "anyhow,backtrace", crate: juniper }
|
||||||
- { feature: bigdecimal, crate: juniper }
|
- { feature: bigdecimal, crate: juniper }
|
||||||
- { feature: bson, crate: juniper }
|
- { feature: bson, crate: juniper }
|
||||||
- { feature: chrono, crate: juniper }
|
- { feature: chrono, crate: juniper }
|
||||||
- { feature: chrono-clock, crate: juniper }
|
- { feature: chrono-clock, crate: juniper }
|
||||||
- { feature: chrono-tz, crate: juniper }
|
- { feature: chrono-tz, crate: juniper }
|
||||||
- { feature: expose-test-schema, crate: juniper }
|
- { feature: expose-test-schema, crate: juniper }
|
||||||
- { feature: graphql-parser, crate: juniper }
|
- { feature: jiff, crate: juniper }
|
||||||
- { feature: rust_decimal, crate: juniper }
|
- { feature: rust_decimal, crate: juniper }
|
||||||
- { feature: schema-language, crate: juniper }
|
- { feature: schema-language, crate: juniper }
|
||||||
- { feature: serde_json, crate: juniper }
|
|
||||||
- { feature: time, crate: juniper }
|
- { feature: time, crate: juniper }
|
||||||
- { feature: url, crate: juniper }
|
- { feature: url, crate: juniper }
|
||||||
- { feature: uuid, crate: juniper }
|
- { feature: uuid, crate: juniper }
|
||||||
|
- { feature: graphql-transport-ws, crate: juniper_graphql_ws }
|
||||||
|
- { feature: graphql-ws, crate: juniper_graphql_ws }
|
||||||
- { feature: <none>, crate: juniper_actix }
|
- { feature: <none>, crate: juniper_actix }
|
||||||
- { feature: subscriptions, crate: juniper_actix }
|
- { feature: subscriptions, crate: juniper_actix }
|
||||||
|
- { feature: <none>, crate: juniper_axum }
|
||||||
|
- { feature: subscriptions, crate: juniper_axum }
|
||||||
- { feature: <none>, crate: juniper_warp }
|
- { feature: <none>, crate: juniper_warp }
|
||||||
- { feature: subscriptions, crate: juniper_warp }
|
- { feature: subscriptions, crate: juniper_warp }
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly
|
||||||
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
override: true
|
|
||||||
|
|
||||||
# TODO: Enable once MSRV is supported.
|
- run: cargo +nightly update -Z minimal-versions
|
||||||
#- run: cargo +nightly update -Z minimal-versions
|
|
||||||
|
|
||||||
- run: cargo check -p ${{ matrix.crate }} --no-default-features
|
- run: cargo check -p ${{ matrix.crate }} --no-default-features
|
||||||
${{ matrix.feature != '<none>'
|
${{ matrix.feature != '<none>'
|
||||||
|
@ -157,23 +140,62 @@ jobs:
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: -D warnings
|
RUSTFLAGS: -D warnings
|
||||||
|
|
||||||
|
msrv:
|
||||||
|
name: MSRV
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
msrv: ["1.75.0"]
|
||||||
|
crate:
|
||||||
|
- juniper_codegen
|
||||||
|
- juniper
|
||||||
|
- juniper_subscriptions
|
||||||
|
- juniper_graphql_ws
|
||||||
|
- juniper_actix
|
||||||
|
- juniper_axum
|
||||||
|
#- juniper_hyper
|
||||||
|
- juniper_rocket
|
||||||
|
- juniper_warp
|
||||||
|
os:
|
||||||
|
- ubuntu
|
||||||
|
- macOS
|
||||||
|
- windows
|
||||||
|
include:
|
||||||
|
- { msrv: "1.79.0", crate: "juniper_hyper", os: "ubuntu" }
|
||||||
|
- { msrv: "1.79.0", crate: "juniper_hyper", os: "macOS" }
|
||||||
|
- { msrv: "1.79.0", crate: "juniper_hyper", os: "windows" }
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly
|
||||||
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.msrv }}
|
||||||
|
|
||||||
|
- run: cargo +nightly update -Z minimal-versions
|
||||||
|
|
||||||
|
- run: make test.cargo crate=${{ matrix.crate }}
|
||||||
|
|
||||||
package:
|
package:
|
||||||
|
name: check (package)
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/juniper') }}
|
if: ${{ startsWith(github.ref, 'refs/tags/juniper') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
|
|
||||||
- name: Parse crate name
|
- name: Parse crate name and version from Git tag
|
||||||
id: crate
|
id: tag
|
||||||
run: echo ::set-output
|
uses: actions-ecosystem/action-regex-match@v2
|
||||||
name=NAME::$(printf "$GITHUB_REF" | cut -d '/' -f3
|
with:
|
||||||
| cut -d '@' -f1)
|
text: ${{ github.ref }}
|
||||||
|
regex: '^refs/tags/(([a-z_]+)-v([0-9]+\.[0-9]+\.[0-9]+(-.+)?))$'
|
||||||
|
|
||||||
- run: cargo package -p ${{ steps.crate.outputs.NAME }}
|
- run: cargo package -p ${{ steps.tag.outputs.group2 }} --all-features
|
||||||
|
|
||||||
test:
|
test:
|
||||||
strategy:
|
strategy:
|
||||||
|
@ -187,8 +209,8 @@ jobs:
|
||||||
- juniper_integration_tests
|
- juniper_integration_tests
|
||||||
- juniper_codegen_tests
|
- juniper_codegen_tests
|
||||||
- juniper_actix
|
- juniper_actix
|
||||||
|
- juniper_axum
|
||||||
- juniper_hyper
|
- juniper_hyper
|
||||||
- juniper_iron
|
|
||||||
- juniper_rocket
|
- juniper_rocket
|
||||||
- juniper_warp
|
- juniper_warp
|
||||||
os:
|
os:
|
||||||
|
@ -200,50 +222,49 @@ jobs:
|
||||||
- beta
|
- beta
|
||||||
- nightly
|
- nightly
|
||||||
exclude:
|
exclude:
|
||||||
- crate: juniper_codegen_tests
|
|
||||||
toolchain: stable
|
|
||||||
- crate: juniper_codegen_tests
|
- crate: juniper_codegen_tests
|
||||||
toolchain: beta
|
toolchain: beta
|
||||||
|
- crate: juniper_codegen_tests
|
||||||
|
toolchain: nightly
|
||||||
- crate: juniper_codegen_tests
|
- crate: juniper_codegen_tests
|
||||||
os: macOS
|
os: macOS
|
||||||
- crate: juniper_codegen_tests
|
- crate: juniper_codegen_tests
|
||||||
os: windows
|
os: windows
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
|
||||||
toolchain: ${{ matrix.toolchain }}
|
toolchain: ${{ matrix.toolchain }}
|
||||||
override: true
|
components: rust-src
|
||||||
|
|
||||||
|
- run: cargo install cargo-careful
|
||||||
|
if: ${{ matrix.toolchain == 'nightly' }}
|
||||||
|
|
||||||
- run: make test.cargo crate=${{ matrix.crate }}
|
- run: make test.cargo crate=${{ matrix.crate }}
|
||||||
|
careful=${{ (matrix.toolchain == 'nightly' && 'yes')
|
||||||
|
|| 'no' }}
|
||||||
|
|
||||||
test-book:
|
test-book:
|
||||||
name: test Book
|
name: test (Book)
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os:
|
os:
|
||||||
- ubuntu
|
- ubuntu
|
||||||
- macOS
|
- macOS
|
||||||
# TODO: Re-enable once rust-lang/rust#99466 is fixed:
|
- windows
|
||||||
# https://github.com/rust-lang/rust/issues/99466
|
|
||||||
#- windows
|
|
||||||
toolchain:
|
toolchain:
|
||||||
- stable
|
- stable
|
||||||
- beta
|
- beta
|
||||||
- nightly
|
- nightly
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
|
||||||
toolchain: ${{ matrix.toolchain }}
|
toolchain: ${{ matrix.toolchain }}
|
||||||
override: true
|
- uses: peaceiris/actions-mdbook@v2
|
||||||
|
|
||||||
- run: cargo install mdbook
|
|
||||||
|
|
||||||
- run: make test.book
|
- run: make test.book
|
||||||
|
|
||||||
|
@ -252,23 +273,30 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
crate:
|
crate:
|
||||||
- juniper_codegen
|
|
||||||
- juniper
|
- juniper
|
||||||
|
- juniper_axum
|
||||||
|
target:
|
||||||
|
- wasm32-unknown-unknown
|
||||||
|
- wasm32-wasip1
|
||||||
toolchain:
|
toolchain:
|
||||||
- stable
|
- stable
|
||||||
- beta
|
- beta
|
||||||
- nightly
|
- nightly
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
|
||||||
toolchain: ${{ matrix.toolchain }}
|
toolchain: ${{ matrix.toolchain }}
|
||||||
target: wasm32-unknown-unknown
|
target: ${{ matrix.target }}
|
||||||
override: true
|
|
||||||
|
|
||||||
- run: cargo check --target wasm32-unknown-unknown -p ${{ matrix.crate }}
|
- name: Switch Cargo workspace to `resolver = "2"`
|
||||||
|
run: sed -i 's/resolver = "1"/resolver = "2"/' Cargo.toml
|
||||||
|
|
||||||
|
- run: cargo check --target ${{ matrix.target }} -p ${{ matrix.crate }}
|
||||||
|
${{ (matrix.crate == 'juniper' && matrix.target == 'wasm32-unknown-unknown')
|
||||||
|
&& '--features js'
|
||||||
|
|| '' }}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -277,8 +305,30 @@ jobs:
|
||||||
# Releasing #
|
# Releasing #
|
||||||
#############
|
#############
|
||||||
|
|
||||||
|
publish:
|
||||||
|
name: publish (crates.io)
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/juniper') }}
|
||||||
|
needs: ["release-github"]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
|
||||||
|
- name: Parse crate name and version from Git tag
|
||||||
|
id: tag
|
||||||
|
uses: actions-ecosystem/action-regex-match@v2
|
||||||
|
with:
|
||||||
|
text: ${{ github.ref }}
|
||||||
|
regex: '^refs/tags/(([a-z_]+)-v([0-9]+\.[0-9]+\.[0-9]+(-.+)?))$'
|
||||||
|
|
||||||
|
- run: cargo publish -p ${{ steps.tag.outputs.group2 }} --all-features
|
||||||
|
env:
|
||||||
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATESIO_TOKEN }}
|
||||||
|
|
||||||
release-check:
|
release-check:
|
||||||
name: Check release automation
|
name: check (release)
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/juniper') }}
|
if: ${{ !startsWith(github.ref, 'refs/tags/juniper') }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
@ -289,16 +339,15 @@ jobs:
|
||||||
- juniper_subscriptions
|
- juniper_subscriptions
|
||||||
- juniper_graphql_ws
|
- juniper_graphql_ws
|
||||||
- juniper_actix
|
- juniper_actix
|
||||||
|
- juniper_axum
|
||||||
- juniper_hyper
|
- juniper_hyper
|
||||||
- juniper_iron
|
|
||||||
- juniper_rocket
|
- juniper_rocket
|
||||||
- juniper_warp
|
- juniper_warp
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: dtolnay/rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
profile: minimal
|
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
|
|
||||||
- run: cargo install cargo-release
|
- run: cargo install cargo-release
|
||||||
|
@ -307,72 +356,49 @@ jobs:
|
||||||
exec=no install=no
|
exec=no install=no
|
||||||
|
|
||||||
release-github:
|
release-github:
|
||||||
name: Release on GitHub
|
name: release (GitHub)
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/juniper') }}
|
||||||
needs:
|
needs:
|
||||||
- bench
|
- bench
|
||||||
- clippy
|
- clippy
|
||||||
- example
|
- codespell
|
||||||
- feature
|
- feature
|
||||||
|
- msrv
|
||||||
- package
|
- package
|
||||||
- rustfmt
|
- rustfmt
|
||||||
- test
|
- test
|
||||||
- test-book
|
- test-book
|
||||||
- wasm
|
- wasm
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/juniper') }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Parse crate name
|
|
||||||
id: crate
|
|
||||||
run: echo ::set-output
|
|
||||||
name=NAME::$(printf "$GITHUB_REF" | cut -d '/' -f3
|
|
||||||
| cut -d '@' -f1)
|
|
||||||
- name: Parse release version
|
|
||||||
id: release
|
|
||||||
run: echo ::set-output
|
|
||||||
name=VERSION::$(printf "$GITHUB_REF" | cut -d '@' -f2)
|
|
||||||
|
|
||||||
|
- name: Parse crate name and version from Git tag
|
||||||
|
id: tag
|
||||||
|
uses: actions-ecosystem/action-regex-match@v2
|
||||||
|
with:
|
||||||
|
text: ${{ github.ref }}
|
||||||
|
regex: '^refs/tags/(([a-z_]+)-v([0-9]+\.[0-9]+\.[0-9]+(-.+)?))$'
|
||||||
- name: Verify release version matches crate's Cargo manifest
|
- name: Verify release version matches crate's Cargo manifest
|
||||||
run: >-
|
run: |
|
||||||
test "${{ steps.release.outputs.VERSION }}" \
|
test "${{ steps.tag.outputs.group3 }}" \
|
||||||
== "$(grep -m1 'version = "' ${{ steps.crate.outputs.NAME }}/Cargo.toml | cut -d '"' -f2)"
|
== "$(grep -m1 'version = "' \
|
||||||
|
${{ steps.tag.outputs.group2 }}/Cargo.toml \
|
||||||
|
| cut -d '"' -f2)"
|
||||||
|
|
||||||
- name: Parse CHANGELOG link
|
- name: Parse CHANGELOG link
|
||||||
id: changelog
|
id: changelog
|
||||||
run: echo ::set-output
|
run: echo "link=${{ github.server_url }}/${{ github.repository }}/blob/${{ steps.tag.outputs.group1 }}/${{ steps.tag.outputs.group2 }}/CHANGELOG.md#$(sed -n '/^## \[${{ steps.tag.outputs.group3 }}\]/{s/^## \[\(.*\)\][^0-9]*\([0-9].*\)/\1--\2/;s/[^0-9a-z-]*//g;p;}' ${{ steps.tag.outputs.group2 }}/CHANGELOG.md)"
|
||||||
name=LINK::${{ github.server_url }}/${{ github.repository }}/blob/${{ steps.crate.outputs.NAME }}%40${{ steps.release.outputs.VERSION }}//${{ steps.crate.outputs.NAME }}/CHANGELOG.md#$(sed -n '/^## \[${{ steps.release.outputs.VERSION }}\]/{s/^## \[\(.*\)\][^0-9]*\([0-9].*\)/\1--\2/;s/[^0-9a-z-]*//g;p;}' ${{ steps.crate.outputs.NAME }}/CHANGELOG.md)
|
>> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- uses: softprops/action-gh-release@v1
|
- name: Create GitHub release
|
||||||
env:
|
uses: softprops/action-gh-release@v2
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.crate.outputs.NAME }} ${{ steps.release.outputs.VERSION }}
|
name: ${{ steps.tag.outputs.group2 }} ${{ steps.tag.outputs.group3 }}
|
||||||
body: |
|
body: |
|
||||||
[API docs](https://docs.rs/${{ steps.crate.outputs.NAME }}/${{ steps.release.outputs.VERSION }})
|
[API docs](https://docs.rs/${{ steps.tag.outputs.group2 }}/${{ steps.tag.outputs.group3 }})
|
||||||
[Changelog](${{ steps.changelog.outputs.LINK }})
|
[Changelog](${{ steps.changelog.outputs.link }})
|
||||||
prerelease: ${{ contains(steps.release.outputs.VERSION, '-') }}
|
prerelease: ${{ contains(steps.tag.outputs.group3, '-') }}
|
||||||
|
|
||||||
release-crate:
|
|
||||||
name: Release on crates.io
|
|
||||||
needs: ["release-github"]
|
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/juniper') }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
profile: minimal
|
|
||||||
toolchain: stable
|
|
||||||
|
|
||||||
- name: Parse crate name
|
|
||||||
id: crate
|
|
||||||
run: echo ::set-output
|
|
||||||
name=NAME::$(printf "$GITHUB_REF" | cut -d '/' -f3
|
|
||||||
| cut -d '@' -f1)
|
|
||||||
|
|
||||||
- name: Publish crate
|
|
||||||
run: cargo publish -p ${{ steps.crate.outputs.NAME }}
|
|
||||||
--token ${{ secrets.CRATESIO_TOKEN }}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -382,21 +408,21 @@ jobs:
|
||||||
##########
|
##########
|
||||||
|
|
||||||
deploy-book:
|
deploy-book:
|
||||||
name: deploy Book
|
name: deploy (Book)
|
||||||
needs: ["test", "test-book"]
|
|
||||||
if: ${{ github.ref == 'refs/heads/master'
|
if: ${{ github.ref == 'refs/heads/master'
|
||||||
|| startsWith(github.ref, 'refs/tags/juniper@') }}
|
|| startsWith(github.ref, 'refs/tags/juniper') }}
|
||||||
|
needs: ["codespell", "test", "test-book"]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: peaceiris/actions-mdbook@v1
|
- uses: peaceiris/actions-mdbook@v2
|
||||||
|
|
||||||
- run: make book.build out=gh-pages${{ (github.ref == 'refs/heads/master'
|
- run: make book.build out=gh-pages${{ (github.ref == 'refs/heads/master'
|
||||||
&& '/master')
|
&& '/master')
|
||||||
|| '' }}
|
|| '' }}
|
||||||
|
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
uses: peaceiris/actions-gh-pages@v3
|
uses: peaceiris/actions-gh-pages@v4
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
keep_files: true
|
keep_files: true
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
|
resolver = "1" # unifying Cargo features asap for Book tests
|
||||||
members = [
|
members = [
|
||||||
"benches",
|
"benches",
|
||||||
"examples/basic_subscriptions",
|
|
||||||
"examples/warp_async",
|
|
||||||
"examples/warp_subscriptions",
|
|
||||||
"examples/actix_subscriptions",
|
|
||||||
"juniper_codegen",
|
"juniper_codegen",
|
||||||
"juniper",
|
"juniper",
|
||||||
"juniper_hyper",
|
"juniper_hyper",
|
||||||
"juniper_iron",
|
|
||||||
"juniper_rocket",
|
"juniper_rocket",
|
||||||
"juniper_subscriptions",
|
"juniper_subscriptions",
|
||||||
"juniper_graphql_ws",
|
"juniper_graphql_ws",
|
||||||
"juniper_warp",
|
"juniper_warp",
|
||||||
"juniper_actix",
|
"juniper_actix",
|
||||||
|
"juniper_axum",
|
||||||
"tests/codegen",
|
"tests/codegen",
|
||||||
"tests/integration",
|
"tests/integration",
|
||||||
]
|
]
|
||||||
|
|
6
LICENSE
6
LICENSE
|
@ -1,6 +1,10 @@
|
||||||
BSD 2-Clause License
|
BSD 2-Clause License
|
||||||
|
|
||||||
Copyright (c) 2016, Magnus Hallin
|
Copyright (c) 2016-2025 Magnus Hallin <mhallin@fastmail.com>,
|
||||||
|
Christoph Herzog <chris@theduke.at>,
|
||||||
|
Christian Legnitto <christian@legnitto.com>,
|
||||||
|
Ilya Solovyiov <ilya.solovyiov@gmail.com>,
|
||||||
|
Kai Ren <tyranron@gmail.com>
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
|
70
Makefile
70
Makefile
|
@ -18,6 +18,9 @@ eq = $(if $(or $(1),$(2)),$(and $(findstring $(1),$(2)),\
|
||||||
book: book.build
|
book: book.build
|
||||||
|
|
||||||
|
|
||||||
|
codespell: book.codespell
|
||||||
|
|
||||||
|
|
||||||
fmt: cargo.fmt
|
fmt: cargo.fmt
|
||||||
|
|
||||||
|
|
||||||
|
@ -52,6 +55,7 @@ cargo.fmt:
|
||||||
|
|
||||||
cargo.lint:
|
cargo.lint:
|
||||||
cargo clippy --workspace --all-features -- -D warnings
|
cargo clippy --workspace --all-features -- -D warnings
|
||||||
|
cargo clippy -p juniper_integration_tests --tests --all-features -- -D warnings
|
||||||
|
|
||||||
|
|
||||||
# Release Rust crate.
|
# Release Rust crate.
|
||||||
|
@ -93,18 +97,32 @@ test.book:
|
||||||
ifeq ($(clean),yes)
|
ifeq ($(clean),yes)
|
||||||
cargo clean
|
cargo clean
|
||||||
endif
|
endif
|
||||||
cargo build
|
$(eval target := $(strip $(shell cargo -vV | sed -n 's/host: //p')))
|
||||||
mdbook test book -L target/debug/deps
|
cargo build --all-features
|
||||||
|
mdbook test book -L target/debug/deps $(strip \
|
||||||
|
$(if $(call eq,$(findstring windows,$(target)),),,\
|
||||||
|
$(shell cargo metadata -q \
|
||||||
|
| jq -r '.packages[] | select(.name == "windows_$(word 1,$(subst -, ,$(target)))_$(word 4,$(subst -, ,$(target)))") | .manifest_path' \
|
||||||
|
| sed -e "s/^/-L '/" -e 's/Cargo.toml/lib/' -e "s/$$/'/" )))
|
||||||
|
|
||||||
|
|
||||||
# Run Rust tests of project crates.
|
# Run Rust tests of project crates.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# make test.cargo [crate=<crate-name>]
|
# make test.cargo [crate=<crate-name>] [careful=(no|yes)]
|
||||||
|
|
||||||
test.cargo:
|
test.cargo:
|
||||||
cargo $(if $(call eq,$(crate),juniper_codegen_tests),+nightly,) test \
|
ifeq ($(careful),yes)
|
||||||
$(if $(call eq,$(crate),),--workspace,-p $(crate)) --all-features
|
ifeq ($(shell cargo install --list | grep cargo-careful),)
|
||||||
|
cargo install cargo-careful
|
||||||
|
endif
|
||||||
|
ifeq ($(shell rustup component list --toolchain=nightly \
|
||||||
|
| grep 'rust-src (installed)'),)
|
||||||
|
rustup component add --toolchain=nightly rust-src
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
cargo $(if $(call eq,$(careful),yes),+nightly careful,) \
|
||||||
|
test $(if $(call eq,$(crate),),--workspace,-p $(crate)) --all-features
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -122,6 +140,15 @@ book.build:
|
||||||
mdbook build book/ $(if $(call eq,$(out),),,-d $(out))
|
mdbook build book/ $(if $(call eq,$(out),),,-d $(out))
|
||||||
|
|
||||||
|
|
||||||
|
# Spellcheck Book.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# make book.codespell [fix=(no|yes)]
|
||||||
|
|
||||||
|
book.codespell:
|
||||||
|
codespell book/ $(if $(call eq,$(fix),yes),--write-changes,)
|
||||||
|
|
||||||
|
|
||||||
# Serve Book on some port.
|
# Serve Book on some port.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
|
@ -133,11 +160,40 @@ book.serve:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
######################
|
||||||
|
# Forwarded commands #
|
||||||
|
######################
|
||||||
|
|
||||||
|
# Download and prepare actual version of GraphiQL static files, used for
|
||||||
|
# integrating it.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# make graphiql
|
||||||
|
|
||||||
|
graphiql:
|
||||||
|
@cd juniper/ && \
|
||||||
|
make graphiql
|
||||||
|
|
||||||
|
|
||||||
|
# Download and prepare actual version of GraphQL Playground static files, used
|
||||||
|
# for integrating it.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# make graphql-playground
|
||||||
|
|
||||||
|
graphql-playground:
|
||||||
|
@cd juniper/ && \
|
||||||
|
make graphql-playground
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##################
|
##################
|
||||||
# .PHONY section #
|
# .PHONY section #
|
||||||
##################
|
##################
|
||||||
|
|
||||||
.PHONY: book fmt lint release test \
|
.PHONY: book codespell fmt lint release test \
|
||||||
book.build book.serve \
|
book.build book.codespell book.serve \
|
||||||
cargo.fmt cargo.lint cargo.release cargo.test \
|
cargo.fmt cargo.lint cargo.release cargo.test \
|
||||||
|
graphiql graphql-playground \
|
||||||
test.book test.cargo
|
test.book test.cargo
|
||||||
|
|
20
README.md
20
README.md
|
@ -5,7 +5,7 @@
|
||||||
[![Build Status](https://dev.azure.com/graphql-rust/GraphQL%20Rust/_apis/build/status/graphql-rust.juniper)](https://dev.azure.com/graphql-rust/GraphQL%20Rust/_build/latest?definitionId=1)
|
[![Build Status](https://dev.azure.com/graphql-rust/GraphQL%20Rust/_apis/build/status/graphql-rust.juniper)](https://dev.azure.com/graphql-rust/GraphQL%20Rust/_build/latest?definitionId=1)
|
||||||
[![codecov](https://codecov.io/gh/graphql-rust/juniper/branch/master/graph/badge.svg)](https://codecov.io/gh/graphql-rust/juniper)
|
[![codecov](https://codecov.io/gh/graphql-rust/juniper/branch/master/graph/badge.svg)](https://codecov.io/gh/graphql-rust/juniper)
|
||||||
[![Crates.io](https://img.shields.io/crates/v/juniper.svg?maxAge=2592000)](https://crates.io/crates/juniper)
|
[![Crates.io](https://img.shields.io/crates/v/juniper.svg?maxAge=2592000)](https://crates.io/crates/juniper)
|
||||||
[![Gitter chat](https://badges.gitter.im/juniper-graphql/gitter.svg)](https://gitter.im/juniper-graphql)
|
[![Gitter chat](https://badges.gitter.im/juniper-graphql/gitter.svg)](https://gitter.im/juniper-graphql/Lobby)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ GraphQL schemas as convenient as Rust will allow.
|
||||||
|
|
||||||
Juniper does not include a web server - instead it provides building blocks to
|
Juniper does not include a web server - instead it provides building blocks to
|
||||||
make integration with existing servers straightforward. It optionally provides a
|
make integration with existing servers straightforward. It optionally provides a
|
||||||
pre-built integration for the [Actix][actix], [Hyper][hyper], [Iron][iron], [Rocket], and [Warp][warp] frameworks, including
|
pre-built integration for the [Actix][actix], [Hyper][hyper], [Rocket], and [Warp][warp] frameworks, including
|
||||||
embedded [Graphiql][graphiql] and [GraphQL Playground][playground] for easy debugging.
|
embedded [Graphiql][graphiql] and [GraphQL Playground][playground] for easy debugging.
|
||||||
|
|
||||||
- [Cargo crate](https://crates.io/crates/juniper)
|
- [Cargo crate](https://crates.io/crates/juniper)
|
||||||
|
@ -42,7 +42,7 @@ For specific information about macros, types and the Juniper api, the
|
||||||
You can also check out the [Star Wars schema][test_schema_rs] to see a complex
|
You can also check out the [Star Wars schema][test_schema_rs] to see a complex
|
||||||
example including polymorphism with traits and interfaces.
|
example including polymorphism with traits and interfaces.
|
||||||
For an example of web framework integration,
|
For an example of web framework integration,
|
||||||
see the [actix][actix_examples], [hyper][hyper_examples], [rocket][rocket_examples], [iron][iron_examples], and [warp][warp_examples] examples folders.
|
see the [actix][actix_examples], [axum][axum_examples], [hyper][hyper_examples], [rocket][rocket_examples], and [warp][warp_examples] examples folders.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -73,15 +73,16 @@ your Schemas automatically.
|
||||||
- [url][url]
|
- [url][url]
|
||||||
- [chrono][chrono]
|
- [chrono][chrono]
|
||||||
- [chrono-tz][chrono-tz]
|
- [chrono-tz][chrono-tz]
|
||||||
|
- [jiff][jiff]
|
||||||
- [time][time]
|
- [time][time]
|
||||||
- [bson][bson]
|
- [bson][bson]
|
||||||
|
|
||||||
### Web Frameworks
|
### Web Frameworks
|
||||||
|
|
||||||
- [actix][actix]
|
- [actix][actix]
|
||||||
|
- [axum][axum]
|
||||||
- [hyper][hyper]
|
- [hyper][hyper]
|
||||||
- [rocket][rocket]
|
- [rocket][rocket]
|
||||||
- [iron][iron]
|
|
||||||
- [warp][warp]
|
- [warp][warp]
|
||||||
|
|
||||||
## Guides & Examples
|
## Guides & Examples
|
||||||
|
@ -93,25 +94,25 @@ your Schemas automatically.
|
||||||
Juniper has not reached 1.0 yet, thus some API instability should be expected.
|
Juniper has not reached 1.0 yet, thus some API instability should be expected.
|
||||||
|
|
||||||
[actix]: https://actix.rs/
|
[actix]: https://actix.rs/
|
||||||
|
[axum]: https://docs.rs/axum
|
||||||
[graphql]: http://graphql.org
|
[graphql]: http://graphql.org
|
||||||
[graphiql]: https://github.com/graphql/graphiql
|
[graphiql]: https://github.com/graphql/graphiql
|
||||||
[playground]: https://github.com/prisma/graphql-playground
|
[playground]: https://github.com/prisma/graphql-playground
|
||||||
[iron]: https://github.com/iron/iron
|
|
||||||
[graphql_spec]: https://spec.graphql.org/October2021
|
[graphql_spec]: https://spec.graphql.org/October2021
|
||||||
[schema_language]: https://graphql.org/learn/schema/#type-language
|
[schema_language]: https://graphql.org/learn/schema/#type-language
|
||||||
[schema_approach]: https://blog.logrocket.com/code-first-vs-schema-first-development-graphql/
|
[schema_approach]: https://blog.logrocket.com/code-first-vs-schema-first-development-graphql/
|
||||||
[test_schema_rs]: https://github.com/graphql-rust/juniper/blob/master/juniper/src/tests/fixtures/starwars/schema.rs
|
[test_schema_rs]: https://github.com/graphql-rust/juniper/blob/master/juniper/src/tests/fixtures/starwars/schema.rs
|
||||||
[tokio]: https://github.com/tokio-rs/tokio
|
[tokio]: https://github.com/tokio-rs/tokio
|
||||||
[actix_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_actix/examples
|
[actix_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_actix/examples
|
||||||
|
[axum_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_axum/examples
|
||||||
[hyper_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_hyper/examples
|
[hyper_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_hyper/examples
|
||||||
[rocket_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_rocket/examples
|
[rocket_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_rocket/examples
|
||||||
[iron_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_iron/examples
|
|
||||||
[hyper]: https://hyper.rs
|
[hyper]: https://hyper.rs
|
||||||
[rocket]: https://rocket.rs
|
[rocket]: https://rocket.rs
|
||||||
[book]: https://graphql-rust.github.io
|
[book]: https://graphql-rust.github.io/juniper
|
||||||
[book_master]: https://graphql-rust.github.io/juniper/master
|
[book_master]: https://graphql-rust.github.io/juniper/master
|
||||||
[book_index]: https://graphql-rust.github.io
|
[book_index]: https://graphql-rust.github.io/juniper
|
||||||
[book_quickstart]: https://graphql-rust.github.io/quickstart.html
|
[book_quickstart]: https://graphql-rust.github.io/juniper/quickstart.html
|
||||||
[docsrs]: https://docs.rs/juniper
|
[docsrs]: https://docs.rs/juniper
|
||||||
[warp]: https://github.com/seanmonstar/warp
|
[warp]: https://github.com/seanmonstar/warp
|
||||||
[warp_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_warp/examples
|
[warp_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_warp/examples
|
||||||
|
@ -119,6 +120,7 @@ Juniper has not reached 1.0 yet, thus some API instability should be expected.
|
||||||
[url]: https://crates.io/crates/url
|
[url]: https://crates.io/crates/url
|
||||||
[chrono]: https://crates.io/crates/chrono
|
[chrono]: https://crates.io/crates/chrono
|
||||||
[chrono-tz]: https://crates.io/crates/chrono-tz
|
[chrono-tz]: https://crates.io/crates/chrono-tz
|
||||||
|
[jiff]: https://crates.io/crates/jiff
|
||||||
[time]: https://crates.io/crates/time
|
[time]: https://crates.io/crates/time
|
||||||
[bson]: https://crates.io/crates/bson
|
[bson]: https://crates.io/crates/bson
|
||||||
[juniper-from-schema]: https://github.com/davidpdrsn/juniper-from-schema
|
[juniper-from-schema]: https://github.com/davidpdrsn/juniper-from-schema
|
||||||
|
|
|
@ -6,11 +6,12 @@ authors = ["Christoph Herzog <chris@theduke.at>"]
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
dataloader = "0.18" # for Book only
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
juniper = { path = "../juniper" }
|
juniper = { path = "../juniper" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = "0.4"
|
criterion = "0.5"
|
||||||
tokio = { version = "1.0", features = ["rt-multi-thread"] }
|
tokio = { version = "1.0", features = ["rt-multi-thread"] }
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
|
|
|
@ -31,9 +31,7 @@ fn bench_sync_vs_async_users_flat_instant(c: &mut Criterion) {
|
||||||
let mut group = c.benchmark_group("Sync vs Async - Users Flat - Instant");
|
let mut group = c.benchmark_group("Sync vs Async - Users Flat - Instant");
|
||||||
for count in [1, 10] {
|
for count in [1, 10] {
|
||||||
group.bench_function(BenchmarkId::new("Sync", count), |b| {
|
group.bench_function(BenchmarkId::new("Sync", count), |b| {
|
||||||
let ids = (0..count)
|
let ids = (0..count).map(InputValue::scalar).collect::<Vec<_>>();
|
||||||
.map(|x| InputValue::scalar(x as i32))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let ids = InputValue::list(ids);
|
let ids = InputValue::list(ids);
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
j::execute_sync(
|
j::execute_sync(
|
||||||
|
@ -48,9 +46,7 @@ fn bench_sync_vs_async_users_flat_instant(c: &mut Criterion) {
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let ids = (0..count)
|
let ids = (0..count).map(InputValue::scalar).collect::<Vec<_>>();
|
||||||
.map(|x| InputValue::scalar(x as i32))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let ids = InputValue::list(ids);
|
let ids = InputValue::list(ids);
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
|
@ -65,9 +61,7 @@ fn bench_sync_vs_async_users_flat_instant(c: &mut Criterion) {
|
||||||
group.bench_function(BenchmarkId::new("Async - Threadpool", count), |b| {
|
group.bench_function(BenchmarkId::new("Async - Threadpool", count), |b| {
|
||||||
let rt = tokio::runtime::Builder::new_multi_thread().build().unwrap();
|
let rt = tokio::runtime::Builder::new_multi_thread().build().unwrap();
|
||||||
|
|
||||||
let ids = (0..count)
|
let ids = (0..count).map(InputValue::scalar).collect::<Vec<_>>();
|
||||||
.map(|x| InputValue::scalar(x as i32))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let ids = InputValue::list(ids);
|
let ids = InputValue::list(ids);
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
|
|
|
@ -45,10 +45,11 @@ The output will be in the `_rendered/` directory.
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
To run the tests validating all code examples in the book, run:
|
To run the tests validating all code examples in the book, run (from project root dir):
|
||||||
```bash
|
```bash
|
||||||
mdbook test -L ../target/debug/deps
|
cargo build
|
||||||
|
mdbook test -L target/debug/deps
|
||||||
|
|
||||||
# or via shortcut from project root dir:
|
# or via shortcut:
|
||||||
make test.book
|
make test.book
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
[book]
|
[book]
|
||||||
title = "Juniper Book (GraphQL server for Rust)"
|
title = "Juniper Book"
|
||||||
description = "User guide for Juniper (GraphQL server library for Rust)."
|
description = "User guide for Juniper (GraphQL server library for Rust)."
|
||||||
language = "en"
|
language = "en"
|
||||||
multilingual = false
|
multilingual = false
|
||||||
|
authors = [
|
||||||
|
"Kai Ren (@tyranron)",
|
||||||
|
]
|
||||||
src = "src"
|
src = "src"
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
|
@ -10,7 +13,7 @@ build-dir = "_rendered"
|
||||||
create-missing = false
|
create-missing = false
|
||||||
|
|
||||||
[output.html]
|
[output.html]
|
||||||
git_repository_url = "https://github.com/graphql-rs/juniper"
|
git_repository_url = "https://github.com/graphql-rust/juniper"
|
||||||
|
|
||||||
[rust]
|
[rust]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
# Juniper
|
|
||||||
|
|
||||||
Juniper is a [GraphQL] server library for Rust. Build type-safe and fast API
|
|
||||||
servers with minimal boilerplate and configuration.
|
|
||||||
|
|
||||||
[GraphQL][graphql] is a data query language developed by Facebook intended to
|
|
||||||
serve mobile and web application frontends.
|
|
||||||
|
|
||||||
_Juniper_ makes it possible to write GraphQL servers in Rust that are
|
|
||||||
type-safe and blazingly fast. We also try to make declaring and resolving
|
|
||||||
GraphQL schemas as convenient as possible as Rust will allow.
|
|
||||||
|
|
||||||
Juniper does not include a web server - instead it provides building blocks to
|
|
||||||
make integration with existing servers straightforward. It optionally provides a
|
|
||||||
pre-built integration for the [Hyper][hyper], [Iron][iron], [Rocket], and [Warp][warp] frameworks, including
|
|
||||||
embedded [Graphiql][graphiql] for easy debugging.
|
|
||||||
|
|
||||||
- [Cargo crate](https://crates.io/crates/juniper)
|
|
||||||
- [API Reference][docsrs]
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
Juniper supports the full GraphQL query language according to the
|
|
||||||
[specification (October 2021)][graphql_spec], including interfaces, unions, schema
|
|
||||||
introspection, and validations.
|
|
||||||
It does not, however, support the schema language.
|
|
||||||
|
|
||||||
As an exception to other GraphQL libraries for other languages, Juniper builds
|
|
||||||
non-null types by default. A field of type `Vec<Episode>` will be converted into
|
|
||||||
`[Episode!]!`. The corresponding Rust type for e.g. `[Episode]` would be
|
|
||||||
`Option<Vec<Option<Episode>>>`.
|
|
||||||
|
|
||||||
## Integrations
|
|
||||||
|
|
||||||
### Data types
|
|
||||||
|
|
||||||
Juniper has automatic integration with some very common Rust crates to make
|
|
||||||
building schemas a breeze. The types from these crates will be usable in
|
|
||||||
your Schemas automatically.
|
|
||||||
|
|
||||||
- [uuid][uuid]
|
|
||||||
- [url][url]
|
|
||||||
- [chrono][chrono]
|
|
||||||
- [bson][bson]
|
|
||||||
|
|
||||||
### Web Frameworks
|
|
||||||
|
|
||||||
- [hyper][hyper]
|
|
||||||
- [rocket][rocket]
|
|
||||||
- [iron][iron]
|
|
||||||
- [warp][warp]
|
|
||||||
|
|
||||||
## API Stability
|
|
||||||
|
|
||||||
Juniper has not reached 1.0 yet, thus some API instability should be expected.
|
|
||||||
|
|
||||||
[graphql]: http://graphql.org
|
|
||||||
[graphiql]: https://github.com/graphql/graphiql
|
|
||||||
[iron]: https://github.com/iron/iron
|
|
||||||
[graphql_spec]: https://spec.graphql.org/October2021
|
|
||||||
[test_schema_rs]: https://github.com/graphql-rust/juniper/blob/master/juniper/src/tests/schema.rs
|
|
||||||
[tokio]: https://github.com/tokio-rs/tokio
|
|
||||||
[hyper_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_hyper/examples
|
|
||||||
[rocket_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_rocket/examples
|
|
||||||
[iron_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_iron/examples
|
|
||||||
[hyper]: https://hyper.rs
|
|
||||||
[rocket]: https://rocket.rs
|
|
||||||
[book]: https://graphql-rust.github.io
|
|
||||||
[book_quickstart]: https://graphql-rust.github.io/quickstart.html
|
|
||||||
[docsrs]: https://docs.rs/juniper
|
|
||||||
[warp]: https://github.com/seanmonstar/warp
|
|
||||||
[warp_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_warp/examples
|
|
||||||
[uuid]: https://crates.io/crates/uuid
|
|
||||||
[url]: https://crates.io/crates/url
|
|
||||||
[chrono]: https://crates.io/crates/chrono
|
|
||||||
[bson]: https://crates.io/crates/bson
|
|
|
@ -1,40 +1,28 @@
|
||||||
- [Introduction](README.md)
|
# Summary
|
||||||
|
|
||||||
|
- [Introduction](introduction.md)
|
||||||
- [Quickstart](quickstart.md)
|
- [Quickstart](quickstart.md)
|
||||||
|
- [Type system](types/index.md)
|
||||||
- [Type System](types/index.md)
|
- [Objects](types/objects/index.md)
|
||||||
|
- [Complex fields](types/objects/complex_fields.md)
|
||||||
- [Defining objects](types/objects/defining_objects.md)
|
- [Context](types/objects/context.md)
|
||||||
- [Complex fields](types/objects/complex_fields.md)
|
- [Error handling](types/objects/error/index.md)
|
||||||
- [Using contexts](types/objects/using_contexts.md)
|
- [Field errors](types/objects/error/field.md)
|
||||||
- [Error handling](types/objects/error_handling.md)
|
- [Schema errors](types/objects/error/schema.md)
|
||||||
- [Other types](types/other-index.md)
|
- [Generics](types/objects/generics.md)
|
||||||
- [Enums](types/enums.md)
|
|
||||||
- [Interfaces](types/interfaces.md)
|
- [Interfaces](types/interfaces.md)
|
||||||
|
- [Unions](types/unions.md)
|
||||||
|
- [Enums](types/enums.md)
|
||||||
- [Input objects](types/input_objects.md)
|
- [Input objects](types/input_objects.md)
|
||||||
- [Scalars](types/scalars.md)
|
- [Scalars](types/scalars.md)
|
||||||
- [Unions](types/unions.md)
|
- [Schema](schema/index.md)
|
||||||
|
- [Subscriptions](schema/subscriptions.md)
|
||||||
- [Schemas and mutations](schema/schemas_and_mutations.md)
|
- [Introspection](schema/introspection.md)
|
||||||
|
- [Serving](serve/index.md)
|
||||||
- [Adding A Server](servers/index.md)
|
- [Batching](serve/batching.md)
|
||||||
|
|
||||||
- [Official Server Integrations](servers/official.md) - [Hyper](servers/hyper.md)
|
|
||||||
- [Warp](servers/warp.md)
|
|
||||||
- [Rocket](servers/rocket.md)
|
|
||||||
- [Iron](servers/iron.md)
|
|
||||||
- [Hyper](servers/hyper.md)
|
|
||||||
- [Third Party Integrations](servers/third-party.md)
|
|
||||||
|
|
||||||
- [Advanced Topics](advanced/index.md)
|
- [Advanced Topics](advanced/index.md)
|
||||||
|
- [Implicit and explicit `null`](advanced/implicit_and_explicit_null.md)
|
||||||
- [Introspection](advanced/introspection.md)
|
- [N+1 problem](advanced/n_plus_1.md)
|
||||||
- [Non-struct objects](advanced/non_struct_objects.md)
|
- [DataLoader](advanced/dataloader.md)
|
||||||
- [Implicit and explicit null](advanced/implicit_and_explicit_null.md)
|
- [Look-ahead](advanced/lookahead.md)
|
||||||
- [Objects and generics](advanced/objects_and_generics.md)
|
- [Eager loading](advanced/eager_loading.md)
|
||||||
- [Multiple operations per request](advanced/multiple_ops_per_request.md)
|
|
||||||
- [Dataloaders](advanced/dataloaders.md)
|
|
||||||
- [Subscriptions](advanced/subscriptions.md)
|
|
||||||
|
|
||||||
# - [Context switching]
|
|
||||||
|
|
||||||
# - [Dynamic type system]
|
|
||||||
|
|
198
book/src/advanced/dataloader.md
Normal file
198
book/src/advanced/dataloader.md
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
DataLoader
|
||||||
|
==========
|
||||||
|
|
||||||
|
DataLoader pattern, named after the correspondent [`dataloader` NPM package][0], represents a mechanism of batching and caching data requests in a delayed manner for solving the [N+1 problem](n_plus_1.md).
|
||||||
|
|
||||||
|
> A port of the "Loader" API originally developed by [@schrockn] at Facebook in 2010 as a simplifying force to coalesce the sundry key-value store back-end APIs which existed at the time. At Facebook, "Loader" became one of the implementation details of the "Ent" framework, a privacy-aware data entity loading and caching layer within web server product code. This ultimately became the underpinning for Facebook's GraphQL server implementation and type definitions.
|
||||||
|
|
||||||
|
In [Rust] ecosystem, DataLoader pattern is introduced with the [`dataloader` crate][1], naturally usable with [Juniper].
|
||||||
|
|
||||||
|
Let's remake our [example of N+1 problem](n_plus_1.md), so it's solved by applying the DataLoader pattern:
|
||||||
|
```rust
|
||||||
|
# extern crate anyhow;
|
||||||
|
# extern crate dataloader;
|
||||||
|
# extern crate juniper;
|
||||||
|
# use std::{collections::HashMap, sync::Arc};
|
||||||
|
# use anyhow::anyhow;
|
||||||
|
# use dataloader::non_cached::Loader;
|
||||||
|
# use juniper::{graphql_object, GraphQLObject};
|
||||||
|
#
|
||||||
|
# type CultId = i32;
|
||||||
|
# type UserId = i32;
|
||||||
|
#
|
||||||
|
# struct Repository;
|
||||||
|
#
|
||||||
|
# impl Repository {
|
||||||
|
# async fn load_cults_by_ids(&self, cult_ids: &[CultId]) -> anyhow::Result<HashMap<CultId, Cult>> { unimplemented!() }
|
||||||
|
# async fn load_all_persons(&self) -> anyhow::Result<Vec<Person>> { unimplemented!() }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
struct Context {
|
||||||
|
repo: Repository,
|
||||||
|
cult_loader: CultLoader,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl juniper::Context for Context {}
|
||||||
|
|
||||||
|
#[derive(Clone, GraphQLObject)]
|
||||||
|
struct Cult {
|
||||||
|
id: CultId,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CultBatcher {
|
||||||
|
repo: Repository,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since `BatchFn` doesn't provide any notion of fallible loading, like
|
||||||
|
// `try_load()` returning `Result<HashMap<K, V>, E>`, we handle possible
|
||||||
|
// errors as loaded values and unpack them later in the resolver.
|
||||||
|
impl dataloader::BatchFn<CultId, Result<Cult, Arc<anyhow::Error>>> for CultBatcher {
|
||||||
|
async fn load(
|
||||||
|
&mut self,
|
||||||
|
cult_ids: &[CultId],
|
||||||
|
) -> HashMap<CultId, Result<Cult, Arc<anyhow::Error>>> {
|
||||||
|
// Effectively performs the following SQL query:
|
||||||
|
// SELECT id, name FROM cults WHERE id IN (${cult_id1}, ${cult_id2}, ...)
|
||||||
|
match self.repo.load_cults_by_ids(cult_ids).await {
|
||||||
|
Ok(found_cults) => {
|
||||||
|
found_cults.into_iter().map(|(id, cult)| (id, Ok(cult))).collect()
|
||||||
|
}
|
||||||
|
// One could choose a different strategy to deal with fallible loads,
|
||||||
|
// like consider values that failed to load as absent, or just panic.
|
||||||
|
// See cksac/dataloader-rs#35 for details:
|
||||||
|
// https://github.com/cksac/dataloader-rs/issues/35
|
||||||
|
Err(e) => {
|
||||||
|
// Since `anyhow::Error` doesn't implement `Clone`, we have to
|
||||||
|
// work around here.
|
||||||
|
let e = Arc::new(e);
|
||||||
|
cult_ids.iter().map(|k| (k.clone(), Err(e.clone()))).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CultLoader = Loader<CultId, Result<Cult, Arc<anyhow::Error>>, CultBatcher>;
|
||||||
|
|
||||||
|
fn new_cult_loader(repo: Repository) -> CultLoader {
|
||||||
|
CultLoader::new(CultBatcher { repo })
|
||||||
|
// Usually a `Loader` will coalesce all individual loads which occur
|
||||||
|
// within a single frame of execution before calling a `BatchFn::load()`
|
||||||
|
// with all the collected keys. However, sometimes this behavior is not
|
||||||
|
// desirable or optimal (perhaps, a request is expected to be spread out
|
||||||
|
// over a few subsequent ticks).
|
||||||
|
// A larger yield count will allow more keys to be appended to the batch,
|
||||||
|
// but will wait longer before the actual load. For more details see:
|
||||||
|
// https://github.com/cksac/dataloader-rs/issues/12
|
||||||
|
// https://github.com/graphql/dataloader#batch-scheduling
|
||||||
|
.with_yield_count(100)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Person {
|
||||||
|
id: UserId,
|
||||||
|
name: String,
|
||||||
|
cult_id: CultId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
#[graphql(context = Context)]
|
||||||
|
impl Person {
|
||||||
|
fn id(&self) -> CultId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
self.name.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cult(&self, ctx: &Context) -> anyhow::Result<Cult> {
|
||||||
|
ctx.cult_loader
|
||||||
|
// Here, we don't run the `CultBatcher::load()` eagerly, but rather
|
||||||
|
// only register the `self.cult_id` value in the `cult_loader` and
|
||||||
|
// wait for other concurrent resolvers to do the same.
|
||||||
|
// The actual batch loading happens once all the resolvers register
|
||||||
|
// their IDs and there is nothing more to execute.
|
||||||
|
.try_load(self.cult_id)
|
||||||
|
.await
|
||||||
|
// The outer error is the `io::Error` returned by `try_load()` if
|
||||||
|
// no value is present in the `HashMap` for the specified
|
||||||
|
// `self.cult_id`, meaning that there is no `Cult` with such ID
|
||||||
|
// in the `Repository`.
|
||||||
|
.map_err(|_| anyhow!("No cult exists for ID `{}`", self.cult_id))?
|
||||||
|
// The inner error is the one returned by the `CultBatcher::load()`
|
||||||
|
// if the `Repository::load_cults_by_ids()` fails, meaning that
|
||||||
|
// running the SQL query failed.
|
||||||
|
.map_err(|arc_err| anyhow!("{arc_err}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Query;
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
#[graphql(context = Context)]
|
||||||
|
impl Query {
|
||||||
|
async fn persons(ctx: &Context) -> anyhow::Result<Vec<Person>> {
|
||||||
|
// Effectively performs the following SQL query:
|
||||||
|
// SELECT id, name, cult_id FROM persons
|
||||||
|
ctx.repo.load_all_persons().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And now, performing a [GraphQL query which lead to N+1 problem](n_plus_1.md)
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
persons {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
cult {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
will lead to efficient [SQL] queries, just as expected:
|
||||||
|
```sql
|
||||||
|
SELECT id, name, cult_id FROM persons;
|
||||||
|
SELECT id, name FROM cults WHERE id IN (1, 2, 3, 4);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Caching
|
||||||
|
|
||||||
|
[`dataloader::cached`] provides a [memoization][2] cache: after `BatchFn::load()` is called once with given keys, the resulting values are cached to eliminate redundant loads.
|
||||||
|
|
||||||
|
DataLoader caching does not replace [Redis], [Memcached], or any other shared application-level cache. DataLoader is first and foremost a data loading mechanism, and its cache only serves the purpose of not repeatedly loading the same data [in the context of a single request][3].
|
||||||
|
|
||||||
|
> **WARNING**: A DataLoader should be created per-request to avoid risk of bugs where one client is able to load cached/batched data from another client outside its authenticated scope. Creating a DataLoader within an individual resolver will prevent batching from occurring and will nullify any benefits of it.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Full example
|
||||||
|
|
||||||
|
For a full example using DataLoaders in [Juniper] check out the [`jayy-lmao/rust-graphql-docker` repository][4].
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[`dataloader::cached`]: https://docs.rs/dataloader/latest/dataloader/cached/index.html
|
||||||
|
[@schrockn]: https://github.com/schrockn
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
[Memcached]: https://memcached.org
|
||||||
|
[Redis]: https://redis.io
|
||||||
|
[Rust]: https://www.rust-lang.org
|
||||||
|
[SQL]: https://en.wikipedia.org/wiki/SQL
|
||||||
|
|
||||||
|
[0]: https://github.com/graphql/dataloader
|
||||||
|
[1]: https://docs.rs/crate/dataloader
|
||||||
|
[2]: https://en.wikipedia.org/wiki/Memoization
|
||||||
|
[3]: https://github.com/graphql/dataloader#caching
|
||||||
|
[4]: https://github.com/jayy-lmao/rust-graphql-docker
|
|
@ -1,194 +0,0 @@
|
||||||
# Avoiding the N+1 Problem With Dataloaders
|
|
||||||
|
|
||||||
A common issue with graphql servers is how the resolvers query their datasource.
|
|
||||||
This issue results in a large number of unneccessary database queries or http requests.
|
|
||||||
Say you were wanting to list a bunch of cults people were in
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
query {
|
|
||||||
persons {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
cult {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
What would be executed by a SQL database would be:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT id, name, cult_id FROM persons;
|
|
||||||
SELECT id, name FROM cults WHERE id = 1;
|
|
||||||
SELECT id, name FROM cults WHERE id = 1;
|
|
||||||
SELECT id, name FROM cults WHERE id = 1;
|
|
||||||
SELECT id, name FROM cults WHERE id = 1;
|
|
||||||
SELECT id, name FROM cults WHERE id = 2;
|
|
||||||
SELECT id, name FROM cults WHERE id = 2;
|
|
||||||
SELECT id, name FROM cults WHERE id = 2;
|
|
||||||
# ...
|
|
||||||
```
|
|
||||||
|
|
||||||
Once the list of users has been returned, a separate query is run to find the cult of each user.
|
|
||||||
You can see how this could quickly become a problem.
|
|
||||||
|
|
||||||
A common solution to this is to introduce a **dataloader**.
|
|
||||||
This can be done with Juniper using the crate [cksac/dataloader-rs](https://github.com/cksac/dataloader-rs), which has two types of dataloaders; cached and non-cached.
|
|
||||||
|
|
||||||
#### Cached Loader
|
|
||||||
DataLoader provides a memoization cache, after .load() is called once with a given key, the resulting value is cached to eliminate redundant loads.
|
|
||||||
|
|
||||||
DataLoader caching does not replace Redis, Memcache, or any other shared application-level cache. DataLoader is first and foremost a data loading mechanism, and its cache only serves the purpose of not repeatedly loading the same data in the context of a single request to your Application. [(read more)](https://github.com/graphql/dataloader#caching)
|
|
||||||
|
|
||||||
### What does it look like?
|
|
||||||
|
|
||||||
!FILENAME Cargo.toml
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
actix-identity = "0.4.0-beta.4"
|
|
||||||
actix-rt = "1.0"
|
|
||||||
actix-web = "2.0"
|
|
||||||
async-trait = "0.1.30"
|
|
||||||
dataloader = "0.12.0"
|
|
||||||
futures = "0.3"
|
|
||||||
juniper = "0.16.0"
|
|
||||||
postgres = "0.15.2"
|
|
||||||
```
|
|
||||||
|
|
||||||
```rust, ignore
|
|
||||||
// use dataloader::cached::Loader;
|
|
||||||
use dataloader::non_cached::Loader;
|
|
||||||
use dataloader::BatchFn;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use postgres::{Connection, TlsMode};
|
|
||||||
use std::env;
|
|
||||||
|
|
||||||
pub fn get_db_conn() -> Connection {
|
|
||||||
let pg_connection_string = env::var("DATABASE_URI").expect("need a db uri");
|
|
||||||
println!("Connecting to {pg_connection_string}");
|
|
||||||
let conn = Connection::connect(&pg_connection_string[..], TlsMode::None).unwrap();
|
|
||||||
println!("Connection is fine");
|
|
||||||
conn
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Cult {
|
|
||||||
pub id: i32,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_cult_by_ids(hashmap: &mut HashMap<i32, Cult>, ids: Vec<i32>) {
|
|
||||||
let conn = get_db_conn();
|
|
||||||
for row in &conn
|
|
||||||
.query("SELECT id, name FROM cults WHERE id = ANY($1)", &[&ids])
|
|
||||||
.unwrap()
|
|
||||||
{
|
|
||||||
let cult = Cult {
|
|
||||||
id: row.get(0),
|
|
||||||
name: row.get(1),
|
|
||||||
};
|
|
||||||
hashmap.insert(cult.id, cult);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct CultBatcher;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl BatchFn<i32, Cult> for CultBatcher {
|
|
||||||
|
|
||||||
// A hashmap is used, as we need to return an array which maps each original key to a Cult.
|
|
||||||
async fn load(&self, keys: &[i32]) -> HashMap<i32, Cult> {
|
|
||||||
println!("load cult batch {keys:?}");
|
|
||||||
let mut cult_hashmap = HashMap::new();
|
|
||||||
get_cult_by_ids(&mut cult_hashmap, keys.to_vec());
|
|
||||||
cult_hashmap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type CultLoader = Loader<i32, Cult, CultBatcher>;
|
|
||||||
|
|
||||||
// To create a new loader
|
|
||||||
pub fn get_loader() -> CultLoader {
|
|
||||||
Loader::new(CultBatcher)
|
|
||||||
// Usually a DataLoader will coalesce all individual loads which occur
|
|
||||||
// within a single frame of execution before calling your batch function with all requested keys.
|
|
||||||
// However sometimes this behavior is not desirable or optimal.
|
|
||||||
// Perhaps you expect requests to be spread out over a few subsequent ticks
|
|
||||||
// See: https://github.com/cksac/dataloader-rs/issues/12
|
|
||||||
// More info: https://github.com/graphql/dataloader#batch-scheduling
|
|
||||||
// A larger yield count will allow more requests to append to batch but will wait longer before actual load.
|
|
||||||
.with_yield_count(100)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[juniper::graphql_object(Context = Context)]
|
|
||||||
impl Cult {
|
|
||||||
// your resolvers
|
|
||||||
|
|
||||||
// To call the dataloader
|
|
||||||
pub async fn cult_by_id(ctx: &Context, id: i32) -> Cult {
|
|
||||||
ctx.cult_loader.load(id).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### How do I call them?
|
|
||||||
|
|
||||||
Once created, a dataloader has the async functions `.load()` and `.load_many()`.
|
|
||||||
In the above example `cult_loader.load(id: i32).await` returns `Cult`. If we had used `cult_loader.load_many(Vec<i32>).await` it would have returned `Vec<Cult>`.
|
|
||||||
|
|
||||||
|
|
||||||
### Where do I create my dataloaders?
|
|
||||||
|
|
||||||
**Dataloaders** should be created per-request to avoid risk of bugs where one user is able to load cached/batched data from another user/ outside of its authenticated scope.
|
|
||||||
Creating dataloaders within individual resolvers will prevent batching from occurring and will nullify the benefits of the dataloader.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
_When you declare your context_
|
|
||||||
```rust, ignore
|
|
||||||
use juniper;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Context {
|
|
||||||
pub cult_loader: CultLoader,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl juniper::Context for Context {}
|
|
||||||
|
|
||||||
impl Context {
|
|
||||||
pub fn new(cult_loader: CultLoader) -> Self {
|
|
||||||
Self {
|
|
||||||
cult_loader
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
_Your handler for GraphQL (Note: instantiating context here keeps it per-request)_
|
|
||||||
```rust, ignore
|
|
||||||
pub async fn graphql(
|
|
||||||
st: web::Data<Arc<Schema>>,
|
|
||||||
data: web::Json<GraphQLRequest>,
|
|
||||||
) -> Result<HttpResponse, Error> {
|
|
||||||
|
|
||||||
// Context setup
|
|
||||||
let cult_loader = get_loader();
|
|
||||||
let ctx = Context::new(cult_loader);
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
let res = data.execute(&st, &ctx).await;
|
|
||||||
let json = serde_json::to_string(&res).map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok()
|
|
||||||
.content_type("application/json")
|
|
||||||
.body(json))
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Further Example:
|
|
||||||
|
|
||||||
For a full example using Dataloaders and Context check out [jayy-lmao/rust-graphql-docker](https://github.com/jayy-lmao/rust-graphql-docker).
|
|
280
book/src/advanced/eager_loading.md
Normal file
280
book/src/advanced/eager_loading.md
Normal file
|
@ -0,0 +1,280 @@
|
||||||
|
Eager loading
|
||||||
|
=============
|
||||||
|
|
||||||
|
As a further evolution of the [dealing with the N+1 problem via look-ahead](lookahead.md#n1-problem), we may systematically remodel [Rust] types mapping to [GraphQL] ones in the way to encourage doing eager preloading of data for its [fields][0] and using the already preloaded data when resolving a particular [field][0].
|
||||||
|
|
||||||
|
At the moment, this approach is represented with the [`juniper-eager-loading`] crate for [Juniper].
|
||||||
|
|
||||||
|
> **NOTE**: Since this library requires [`juniper-from-schema`], it's best first to become familiar with it.
|
||||||
|
|
||||||
|
<!-- TODO: Provide example of solving the problem from "N+1 chapter" once `juniper-eager-loading` support the latest `juniper`. -->
|
||||||
|
|
||||||
|
From ["How this library works at a high level"][11] and ["A real example"][12] sections of [`juniper-eager-loading`] documentation:
|
||||||
|
|
||||||
|
> ### How this library works at a high level
|
||||||
|
>
|
||||||
|
> If you have a GraphQL type like this
|
||||||
|
>
|
||||||
|
> ```graphql
|
||||||
|
> type User {
|
||||||
|
> id: Int!
|
||||||
|
> country: Country!
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> You might create the corresponding Rust model type like this:
|
||||||
|
>
|
||||||
|
> ```rust
|
||||||
|
> struct User {
|
||||||
|
> id: i32,
|
||||||
|
> country_id: i32,
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> However this approach has one big issue. How are you going to resolve the field `User.country`
|
||||||
|
> without doing a database query? All the resolver has access to is a `User` with a `country_id`
|
||||||
|
> field. It can't get the country without loading it from the database...
|
||||||
|
>
|
||||||
|
> Fundamentally these kinds of model structs don't work for eager loading with GraphQL. So
|
||||||
|
> this library takes a different approach.
|
||||||
|
>
|
||||||
|
> What if we created separate structs for the database models and the GraphQL models? Something
|
||||||
|
> like this:
|
||||||
|
>
|
||||||
|
> ```rust
|
||||||
|
> # fn main() {}
|
||||||
|
> #
|
||||||
|
> mod models {
|
||||||
|
> pub struct User {
|
||||||
|
> id: i32,
|
||||||
|
> country_id: i32
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> pub struct Country {
|
||||||
|
> id: i32,
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> struct User {
|
||||||
|
> user: models::User,
|
||||||
|
> country: HasOne<Country>,
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> struct Country {
|
||||||
|
> country: models::Country
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> enum HasOne<T> {
|
||||||
|
> Loaded(T),
|
||||||
|
> NotLoaded,
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> Now we're able to resolve the query with code like this:
|
||||||
|
>
|
||||||
|
> 1. Load all the users (first query).
|
||||||
|
> 2. Map the users to a list of country ids.
|
||||||
|
> 3. Load all the countries with those ids (second query).
|
||||||
|
> 4. Pair up the users with the country with the correct id, so change `User.country` from
|
||||||
|
> `HasOne::NotLoaded` to `HasOne::Loaded(matching_country)`.
|
||||||
|
> 5. When resolving the GraphQL field `User.country` simply return the loaded country.
|
||||||
|
>
|
||||||
|
> ### A real example
|
||||||
|
>
|
||||||
|
> ```rust,ignore
|
||||||
|
> use juniper::{Executor, FieldResult};
|
||||||
|
> use juniper_eager_loading::{prelude::*, EagerLoading, HasOne};
|
||||||
|
> use juniper_from_schema::graphql_schema;
|
||||||
|
> use std::error::Error;
|
||||||
|
>
|
||||||
|
> // Define our GraphQL schema.
|
||||||
|
> graphql_schema! {
|
||||||
|
> schema {
|
||||||
|
> query: Query
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> type Query {
|
||||||
|
> allUsers: [User!]! @juniper(ownership: "owned")
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> type User {
|
||||||
|
> id: Int!
|
||||||
|
> country: Country!
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> type Country {
|
||||||
|
> id: Int!
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> // Our model types.
|
||||||
|
> mod models {
|
||||||
|
> use std::error::Error;
|
||||||
|
> use juniper_eager_loading::LoadFrom;
|
||||||
|
>
|
||||||
|
> #[derive(Clone)]
|
||||||
|
> pub struct User {
|
||||||
|
> pub id: i32,
|
||||||
|
> pub country_id: i32
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> #[derive(Clone)]
|
||||||
|
> pub struct Country {
|
||||||
|
> pub id: i32,
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> // This trait is required for eager loading countries.
|
||||||
|
> // It defines how to load a list of countries from a list of ids.
|
||||||
|
> // Notice that `Context` is generic and can be whatever you want.
|
||||||
|
> // It will normally be your Juniper context which would contain
|
||||||
|
> // a database connection.
|
||||||
|
> impl LoadFrom<i32> for Country {
|
||||||
|
> type Error = Box<dyn Error>;
|
||||||
|
> type Context = super::Context;
|
||||||
|
>
|
||||||
|
> fn load(
|
||||||
|
> employments: &[i32],
|
||||||
|
> field_args: &(),
|
||||||
|
> ctx: &Self::Context,
|
||||||
|
> ) -> Result<Vec<Self>, Self::Error> {
|
||||||
|
> // ...
|
||||||
|
> # unimplemented!()
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> // Our sample database connection type.
|
||||||
|
> pub struct DbConnection;
|
||||||
|
>
|
||||||
|
> impl DbConnection {
|
||||||
|
> // Function that will load all the users.
|
||||||
|
> fn load_all_users(&self) -> Vec<models::User> {
|
||||||
|
> // ...
|
||||||
|
> # unimplemented!()
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> // Our Juniper context type which contains a database connection.
|
||||||
|
> pub struct Context {
|
||||||
|
> db: DbConnection,
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> impl juniper::Context for Context {}
|
||||||
|
>
|
||||||
|
> // Our GraphQL user type.
|
||||||
|
> // `#[derive(EagerLoading)]` takes care of generating all the boilerplate code.
|
||||||
|
> #[derive(Clone, EagerLoading)]
|
||||||
|
> // You need to set the context and error type.
|
||||||
|
> #[eager_loading(
|
||||||
|
> context = Context,
|
||||||
|
> error = Box<dyn Error>,
|
||||||
|
>
|
||||||
|
> // These match the default so you wouldn't have to specify them
|
||||||
|
> model = models::User,
|
||||||
|
> id = i32,
|
||||||
|
> root_model_field = user,
|
||||||
|
> )]
|
||||||
|
> pub struct User {
|
||||||
|
> // This user model is used to resolve `User.id`
|
||||||
|
> user: models::User,
|
||||||
|
>
|
||||||
|
> // Setup a "has one" association between a user and a country.
|
||||||
|
> //
|
||||||
|
> // We could also have used `#[has_one(default)]` here.
|
||||||
|
> #[has_one(
|
||||||
|
> foreign_key_field = country_id,
|
||||||
|
> root_model_field = country,
|
||||||
|
> graphql_field = country,
|
||||||
|
> )]
|
||||||
|
> country: HasOne<Country>,
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> // And the GraphQL country type.
|
||||||
|
> #[derive(Clone, EagerLoading)]
|
||||||
|
> #[eager_loading(context = Context, error = Box<dyn Error>)]
|
||||||
|
> pub struct Country {
|
||||||
|
> country: models::Country,
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> // The root query GraphQL type.
|
||||||
|
> pub struct Query;
|
||||||
|
>
|
||||||
|
> impl QueryFields for Query {
|
||||||
|
> // The resolver for `Query.allUsers`.
|
||||||
|
> fn field_all_users(
|
||||||
|
> &self,
|
||||||
|
> executor: &Executor<'_, Context>,
|
||||||
|
> trail: &QueryTrail<'_, User, Walked>,
|
||||||
|
> ) -> FieldResult<Vec<User>> {
|
||||||
|
> let ctx = executor.context();
|
||||||
|
>
|
||||||
|
> // Load the model users.
|
||||||
|
> let user_models = ctx.db.load_all_users();
|
||||||
|
>
|
||||||
|
> // Turn the model users into GraphQL users.
|
||||||
|
> let mut users = User::from_db_models(&user_models);
|
||||||
|
>
|
||||||
|
> // Perform the eager loading.
|
||||||
|
> // `trail` is used to only eager load the fields that are requested. Because
|
||||||
|
> // we're using `QueryTrail`s from "juniper_from_schema" it would be a compile
|
||||||
|
> // error if we eager loaded associations that aren't requested in the query.
|
||||||
|
> User::eager_load_all_children_for_each(&mut users, &user_models, ctx, trail)?;
|
||||||
|
>
|
||||||
|
> Ok(users)
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> impl UserFields for User {
|
||||||
|
> fn field_id(
|
||||||
|
> &self,
|
||||||
|
> executor: &Executor<'_, Context>,
|
||||||
|
> ) -> FieldResult<&i32> {
|
||||||
|
> Ok(&self.user.id)
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> fn field_country(
|
||||||
|
> &self,
|
||||||
|
> executor: &Executor<'_, Context>,
|
||||||
|
> trail: &QueryTrail<'_, Country, Walked>,
|
||||||
|
> ) -> FieldResult<&Country> {
|
||||||
|
> // This will unwrap the country from the `HasOne` or return an error if the
|
||||||
|
> // country wasn't loaded, or wasn't found in the database.
|
||||||
|
> Ok(self.country.try_unwrap()?)
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
>
|
||||||
|
> impl CountryFields for Country {
|
||||||
|
> fn field_id(
|
||||||
|
> &self,
|
||||||
|
> executor: &Executor<'_, Context>,
|
||||||
|
> ) -> FieldResult<&i32> {
|
||||||
|
> Ok(&self.country.id)
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
> #
|
||||||
|
> # fn main() {}
|
||||||
|
> ```
|
||||||
|
|
||||||
|
For more details, check out the [`juniper-eager-loading` documentation][`juniper-eager-loading`].
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Full example
|
||||||
|
|
||||||
|
For a full example using eager loading in [Juniper] check out the [`davidpdrsn/graphql-app-example` repository][10].
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[`juniper-eager-loading`]: https://docs.rs/juniper-eager-loading
|
||||||
|
[`juniper-from-schema`]: https://docs.rs/juniper-from-schema
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
[Redis]: https://redis.io
|
||||||
|
[Rust]: https://www.rust-lang.org
|
||||||
|
|
||||||
|
[0]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||||
|
[10]: https://github.com/davidpdrsn/graphql-app-example
|
||||||
|
[11]: https://docs.rs/juniper-eager-loading/latest/juniper_eager_loading#how-this-library-works-at-a-high-level
|
||||||
|
[12]: https://docs.rs/juniper-eager-loading/latest/juniper_eager_loading#a-real-example
|
|
@ -1,101 +1,89 @@
|
||||||
# Implicit and explicit null
|
Implicit and explicit `null`
|
||||||
|
============================
|
||||||
|
|
||||||
There are two ways that a client can submit a null argument or field in a query.
|
> [GraphQL] has two semantically different ways to represent the lack of a value:
|
||||||
|
> - Explicitly providing the literal value: **null**.
|
||||||
|
> - Implicitly not providing a value at all.
|
||||||
|
|
||||||
They can use a null literal:
|
There are two ways that a client can submit a [`null` value][0] as an [argument][5] or a [field][4] in a [GraphQL] query:
|
||||||
|
1. Either use an explicit `null` literal:
|
||||||
|
```graphql
|
||||||
|
{
|
||||||
|
field(arg: null)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
2. Or simply omit the [argument][5], so the implicit default `null` value kicks in:
|
||||||
|
```graphql
|
||||||
|
{
|
||||||
|
field
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
```graphql
|
There are some situations where it's useful to know which one exactly has been provided.
|
||||||
{
|
|
||||||
field(arg: null)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Or they can simply omit the argument:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
{
|
|
||||||
field
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The former is an explicit null and the latter is an implicit null.
|
|
||||||
|
|
||||||
There are some situations where it's useful to know which one the user provided.
|
|
||||||
|
|
||||||
For example, let's say your business logic has a function that allows users to
|
|
||||||
perform a "patch" operation on themselves. Let's say your users can optionally
|
|
||||||
have favorite and least favorite numbers, and the input for that might look
|
|
||||||
like this:
|
|
||||||
|
|
||||||
|
For example, let's say we have a function that allows users to perform a "patch" operation on themselves. Let's say our users can optionally have favorite and least favorite numbers, and the input for that might look like this:
|
||||||
```rust
|
```rust
|
||||||
/// Updates user attributes. Fields that are `None` are left as-is.
|
/// Updates user attributes. Fields that are [`None`] are left as-is.
|
||||||
pub struct UserPatch {
|
struct UserPatch {
|
||||||
/// If `Some`, updates the user's favorite number.
|
/// If [`Some`], updates the user's favorite number.
|
||||||
pub favorite_number: Option<Option<i32>>,
|
favorite_number: Option<Option<i32>>,
|
||||||
|
|
||||||
/// If `Some`, updates the user's least favorite number.
|
/// If [`Some`], updates the user's least favorite number.
|
||||||
pub least_favorite_number: Option<Option<i32>>,
|
least_favorite_number: Option<Option<i32>>,
|
||||||
}
|
}
|
||||||
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
To set a user's favorite number to 7, you would set `favorite_number` to
|
To set a user's favorite number to 7, we would set `favorite_number` to `Some(Some(7))`. In [GraphQL], that might look like this:
|
||||||
`Some(Some(7))`. In GraphQL, that might look like this:
|
|
||||||
|
|
||||||
```graphql
|
```graphql
|
||||||
mutation { patchUser(patch: { favoriteNumber: 7 }) }
|
mutation { patchUser(patch: { favoriteNumber: 7 }) }
|
||||||
```
|
```
|
||||||
|
|
||||||
To unset the user's favorite number, you would set `favorite_number` to
|
To unset the user's favorite number, we would set `favorite_number` to `Some(None)`. In [GraphQL], that might look like this:
|
||||||
`Some(None)`. In GraphQL, that might look like this:
|
|
||||||
|
|
||||||
```graphql
|
```graphql
|
||||||
mutation { patchUser(patch: { favoriteNumber: null }) }
|
mutation { patchUser(patch: { favoriteNumber: null }) }
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to leave the user's favorite number alone, you would set it to
|
And if we want to leave the user's favorite number alone, just set it to `None`. In [GraphQL], that might look like this:
|
||||||
`None`. In GraphQL, that might look like this:
|
|
||||||
|
|
||||||
```graphql
|
```graphql
|
||||||
mutation { patchUser(patch: {}) }
|
mutation { patchUser(patch: {}) }
|
||||||
```
|
```
|
||||||
|
|
||||||
The last two cases rely on being able to distinguish between explicit and implicit null.
|
The last two cases rely on being able to distinguish between [explicit and implicit `null`][1].
|
||||||
|
|
||||||
In Juniper, this can be done using the `Nullable` type:
|
|
||||||
|
|
||||||
|
Unfortunately, plain `Option` is not capable to distinguish them. That's why in [Juniper], this can be done using the [`Nullable`] type:
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
use juniper::{FieldResult, Nullable};
|
use juniper::{graphql_object, FieldResult, GraphQLInputObject, Nullable};
|
||||||
|
|
||||||
#[derive(juniper::GraphQLInputObject)]
|
#[derive(GraphQLInputObject)]
|
||||||
struct UserPatchInput {
|
struct UserPatchInput {
|
||||||
pub favorite_number: Nullable<i32>,
|
favorite_number: Nullable<i32>,
|
||||||
pub least_favorite_number: Nullable<i32>,
|
least_favorite_number: Nullable<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<UserPatch> for UserPatchInput {
|
impl From<UserPatchInput> for UserPatch {
|
||||||
fn into(self) -> UserPatch {
|
fn from(input: UserPatchInput) -> Self {
|
||||||
UserPatch {
|
Self {
|
||||||
// The `explicit` function transforms the `Nullable` into an
|
// The `explicit()` function transforms the `Nullable` into an
|
||||||
// `Option<Option<T>>` as expected by the business logic layer.
|
// `Option<Option<T>>` as expected by the business logic layer.
|
||||||
favorite_number: self.favorite_number.explicit(),
|
favorite_number: input.favorite_number.explicit(),
|
||||||
least_favorite_number: self.least_favorite_number.explicit(),
|
least_favorite_number: input.least_favorite_number.explicit(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# pub struct UserPatch {
|
# struct UserPatch {
|
||||||
# pub favorite_number: Option<Option<i32>>,
|
# favorite_number: Option<Option<i32>>,
|
||||||
# pub least_favorite_number: Option<Option<i32>>,
|
# least_favorite_number: Option<Option<i32>>,
|
||||||
# }
|
# }
|
||||||
|
#
|
||||||
# struct Session;
|
# struct Session;
|
||||||
# impl Session {
|
# impl Session {
|
||||||
# fn patch_user(&self, _patch: UserPatch) -> FieldResult<()> { Ok(()) }
|
# fn patch_user(&self, _patch: UserPatch) -> FieldResult<()> { Ok(()) }
|
||||||
# }
|
# }
|
||||||
|
#
|
||||||
struct Context {
|
struct Context {
|
||||||
session: Session,
|
session: Session,
|
||||||
}
|
}
|
||||||
|
@ -103,15 +91,27 @@ impl juniper::Context for Context {}
|
||||||
|
|
||||||
struct Mutation;
|
struct Mutation;
|
||||||
|
|
||||||
#[juniper::graphql_object(context = Context)]
|
#[graphql_object]
|
||||||
|
#[graphql(context = Context)]
|
||||||
impl Mutation {
|
impl Mutation {
|
||||||
fn patch_user(ctx: &Context, patch: UserPatchInput) -> FieldResult<bool> {
|
fn patch_user(patch: UserPatchInput, ctx: &Context) -> FieldResult<bool> {
|
||||||
ctx.session.patch_user(patch.into())?;
|
ctx.session.patch_user(patch.into())?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
This type functions much like `Option`, but has two empty variants so you can
|
|
||||||
distinguish between implicit and explicit null.
|
|
||||||
|
|
||||||
|
[`Nullable`]: https://docs.rs/juniper/0.16.1/juniper/enum.Nullable.html
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
[Rust]: https://www.rust-lang.org
|
||||||
|
|
||||||
|
[0]: https://spec.graphql.org/October2021#sec-Null-Value
|
||||||
|
[1]: https://spec.graphql.org/October2021#sel-EAFdRDHAAEJDAoBxzT
|
||||||
|
[4]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||||
|
[5]: https://spec.graphql.org/October2021#sec-Language.Arguments
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
# Advanced Topics
|
Advanced topics
|
||||||
|
===============
|
||||||
|
|
||||||
The chapters below cover some more advanced scenarios.
|
The chapters below cover some more advanced topics.
|
||||||
|
|
||||||
- [Introspection](introspection.md)
|
- [Implicit and explicit `null`](implicit_and_explicit_null.md)
|
||||||
- [Non-struct objects](non_struct_objects.md)
|
- [N+1 problem](n_plus_1.md)
|
||||||
- [Implicit and explicit null](implicit_and_explicit_null.md)
|
- [DataLoader](dataloader.md)
|
||||||
- [Objects and generics](objects_and_generics.md)
|
- [Look-ahead](lookahead.md)
|
||||||
- [Multiple operations per request](multiple_ops_per_request.md)
|
- [Eager loading](eager_loading.md)
|
||||||
- [Dataloaders](dataloaders.md)
|
|
||||||
- [Subscriptions](subscriptions.md)
|
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
# Introspection
|
|
||||||
|
|
||||||
GraphQL defines a special built-in top-level field called `__schema`. Querying
|
|
||||||
for this field allows one to [introspect the schema](https://graphql.org/learn/introspection/)
|
|
||||||
at runtime to see what queries and mutations the GraphQL server supports.
|
|
||||||
|
|
||||||
Because introspection queries are just regular GraphQL queries, Juniper supports
|
|
||||||
them natively. For example, to get all the names of the types supported one
|
|
||||||
could execute the following query against Juniper:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
{
|
|
||||||
__schema {
|
|
||||||
types {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Schema introspection output as JSON
|
|
||||||
|
|
||||||
Many client libraries and tools in the GraphQL ecosystem require a complete
|
|
||||||
representation of the server schema. Often this representation is in JSON and
|
|
||||||
referred to as `schema.json`. A complete representation of the schema can be
|
|
||||||
produced by issuing a specially crafted introspection query.
|
|
||||||
|
|
||||||
Juniper provides a convenience function to introspect the entire schema. The
|
|
||||||
result can then be converted to JSON for use with tools and libraries such as
|
|
||||||
[graphql-client](https://github.com/graphql-rust/graphql-client):
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# #![allow(unused_variables)]
|
|
||||||
# extern crate juniper;
|
|
||||||
# extern crate serde_json;
|
|
||||||
use juniper::{
|
|
||||||
graphql_object, EmptyMutation, EmptySubscription, FieldResult,
|
|
||||||
GraphQLObject, IntrospectionFormat,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Define our schema.
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
struct Example {
|
|
||||||
id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Context;
|
|
||||||
impl juniper::Context for Context {}
|
|
||||||
|
|
||||||
struct Query;
|
|
||||||
|
|
||||||
#[graphql_object(context = Context)]
|
|
||||||
impl Query {
|
|
||||||
fn example(id: String) -> FieldResult<Example> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Schema = juniper::RootNode<
|
|
||||||
'static,
|
|
||||||
Query,
|
|
||||||
EmptyMutation<Context>,
|
|
||||||
EmptySubscription<Context>
|
|
||||||
>;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
// Create a context object.
|
|
||||||
let ctx = Context;
|
|
||||||
|
|
||||||
// Run the built-in introspection query.
|
|
||||||
let (res, _errors) = juniper::introspect(
|
|
||||||
&Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()),
|
|
||||||
&ctx,
|
|
||||||
IntrospectionFormat::default(),
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
// Convert introspection result to json.
|
|
||||||
let json_result = serde_json::to_string_pretty(&res);
|
|
||||||
assert!(json_result.is_ok());
|
|
||||||
}
|
|
||||||
```
|
|
229
book/src/advanced/lookahead.md
Normal file
229
book/src/advanced/lookahead.md
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
Look-ahead
|
||||||
|
==========
|
||||||
|
|
||||||
|
> In backtracking algorithms, **look ahead** is the generic term for a subprocedure that attempts to foresee the effects of choosing a branching variable to evaluate one of its values. The two main aims of look-ahead are to choose a variable to evaluate next and to choose the order of values to assign to it.
|
||||||
|
|
||||||
|
In [GraphQL], look-ahead machinery allows us to introspect the currently [executed][1] [GraphQL operation][2] to see which [fields][3] has been actually selected by it.
|
||||||
|
|
||||||
|
In [Juniper], it's represented by the [`Executor::look_ahead()`][20] method.
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::{graphql_object, Executor, GraphQLObject, ScalarValue};
|
||||||
|
#
|
||||||
|
# type UserId = i32;
|
||||||
|
#
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
struct Person {
|
||||||
|
id: UserId,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Query;
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
// NOTICE: Specifying `ScalarValue` as custom named type parameter,
|
||||||
|
// so its name is similar to the one used in methods.
|
||||||
|
#[graphql(scalar = S: ScalarValue)]
|
||||||
|
impl Query {
|
||||||
|
fn persons<S: ScalarValue>(executor: &Executor<'_, '_, (), S>) -> Vec<Person> {
|
||||||
|
// Let's see which `Person`'s fields were selected in the client query.
|
||||||
|
for field_name in executor.look_ahead().children().names() {
|
||||||
|
dbg!(field_name);
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
# unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
> **TIP**: `S: ScalarValue` type parameter on the method is required here to keep the [`Executor`] being generic over [`ScalarValue`] types. We, instead, could have used the [`DefaultScalarValue`], which is the default [`ScalarValue`] type for the [`Executor`], and make our code more ergonomic, but less flexible and generic.
|
||||||
|
> ```rust
|
||||||
|
> # extern crate juniper;
|
||||||
|
> # use juniper::{graphql_object, DefaultScalarValue, Executor, GraphQLObject};
|
||||||
|
> #
|
||||||
|
> # type UserId = i32;
|
||||||
|
> #
|
||||||
|
> # #[derive(GraphQLObject)]
|
||||||
|
> # struct Person {
|
||||||
|
> # id: UserId,
|
||||||
|
> # name: String,
|
||||||
|
> # }
|
||||||
|
> #
|
||||||
|
> # struct Query;
|
||||||
|
> #
|
||||||
|
> #[graphql_object]
|
||||||
|
> #[graphql(scalar = DefaultScalarValue)]
|
||||||
|
> impl Query {
|
||||||
|
> fn persons(executor: &Executor<'_, '_, ()>) -> Vec<Person> {
|
||||||
|
> for field_name in executor.look_ahead().children().names() {
|
||||||
|
> dbg!(field_name);
|
||||||
|
> }
|
||||||
|
> // ...
|
||||||
|
> # unimplemented!()
|
||||||
|
> }
|
||||||
|
> }
|
||||||
|
> ```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## N+1 problem
|
||||||
|
|
||||||
|
Naturally, look-ahead machinery allows us to solve [the N+1 problem](n_plus_1.md) by introspecting the requested fields and performing loading in batches eagerly, before actual resolving of those fields:
|
||||||
|
```rust
|
||||||
|
# extern crate anyhow;
|
||||||
|
# extern crate juniper;
|
||||||
|
# use std::collections::HashMap;
|
||||||
|
# use anyhow::anyhow;
|
||||||
|
# use juniper::{graphql_object, Executor, GraphQLObject, ScalarValue};
|
||||||
|
#
|
||||||
|
# type CultId = i32;
|
||||||
|
# type UserId = i32;
|
||||||
|
#
|
||||||
|
# struct Repository;
|
||||||
|
#
|
||||||
|
# impl juniper::Context for Repository {}
|
||||||
|
#
|
||||||
|
# impl Repository {
|
||||||
|
# async fn load_cult_by_id(&self, cult_id: CultId) -> anyhow::Result<Option<Cult>> { unimplemented!() }
|
||||||
|
# async fn load_cults_by_ids(&self, cult_ids: &[CultId]) -> anyhow::Result<HashMap<CultId, Cult>> { unimplemented!() }
|
||||||
|
# async fn load_all_persons(&self) -> anyhow::Result<Vec<Person>> { unimplemented!() }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# enum Either<L, R> {
|
||||||
|
# Absent(L),
|
||||||
|
# Loaded(R),
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
#[derive(Clone, GraphQLObject)]
|
||||||
|
struct Cult {
|
||||||
|
id: CultId,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Person {
|
||||||
|
id: UserId,
|
||||||
|
name: String,
|
||||||
|
cult: Either<CultId, Cult>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
#[graphql(context = Repository)]
|
||||||
|
impl Person {
|
||||||
|
fn id(&self) -> CultId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
self.name.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cult(&self, #[graphql(ctx)] repo: &Repository) -> anyhow::Result<Cult> {
|
||||||
|
match &self.cult {
|
||||||
|
Either::Loaded(cult) => Ok(cult.clone()),
|
||||||
|
Either::Absent(cult_id) => {
|
||||||
|
// Effectively performs the following SQL query:
|
||||||
|
// SELECT id, name FROM cults WHERE id = ${cult_id} LIMIT 1
|
||||||
|
repo.load_cult_by_id(*cult_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("No cult exists for ID `{cult_id}`"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Query;
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
#[graphql(context = Repository, scalar = S: ScalarValue)]
|
||||||
|
impl Query {
|
||||||
|
async fn persons<S: ScalarValue>(
|
||||||
|
#[graphql(ctx)] repo: &Repository,
|
||||||
|
executor: &Executor<'_, '_, Repository, S>,
|
||||||
|
) -> anyhow::Result<Vec<Person>> {
|
||||||
|
// Effectively performs the following SQL query:
|
||||||
|
// SELECT id, name, cult_id FROM persons
|
||||||
|
let mut persons = repo.load_all_persons().await?;
|
||||||
|
|
||||||
|
// If the `Person.cult` field has been requested.
|
||||||
|
if executor.look_ahead()
|
||||||
|
.children()
|
||||||
|
.iter()
|
||||||
|
.any(|sel| sel.field_original_name() == "cult")
|
||||||
|
{
|
||||||
|
// Gather `Cult.id`s to load eagerly.
|
||||||
|
let cult_ids = persons
|
||||||
|
.iter()
|
||||||
|
.filter_map(|p| {
|
||||||
|
match &p.cult {
|
||||||
|
Either::Absent(cult_id) => Some(*cult_id),
|
||||||
|
// If for some reason a `Cult` is already loaded,
|
||||||
|
// then just skip it.
|
||||||
|
Either::Loaded(_) => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Load the necessary `Cult`s eagerly.
|
||||||
|
// Effectively performs the following SQL query:
|
||||||
|
// SELECT id, name FROM cults WHERE id IN (${cult_id1}, ${cult_id2}, ...)
|
||||||
|
let cults = repo.load_cults_by_ids(&cult_ids).await?;
|
||||||
|
|
||||||
|
// Populate `persons` with the loaded `Cult`s, so they do not perform
|
||||||
|
// any SQL queries on resolving.
|
||||||
|
for p in &mut persons {
|
||||||
|
let Either::Absent(cult_id) = &p.cult else { continue; };
|
||||||
|
p.cult = Either::Loaded(
|
||||||
|
cults.get(cult_id)
|
||||||
|
.ok_or_else(|| anyhow!("No cult exists for ID `{cult_id}`"))?
|
||||||
|
.clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(persons)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
And so, performing a [GraphQL query which lead to N+1 problem](n_plus_1.md)
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
persons {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
cult {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
will lead to efficient [SQL] queries, just as expected:
|
||||||
|
```sql
|
||||||
|
SELECT id, name, cult_id FROM persons;
|
||||||
|
SELECT id, name FROM cults WHERE id IN (1, 2, 3, 4);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## More features
|
||||||
|
|
||||||
|
See more available look-ahead features in the API docs of the [`LookAheadSelection`][21] and the [`LookAheadChildren`][22].
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[`DefaultScalarValue`]: https://docs.rs/juniper/0.16.1/juniper/enum.DefaultScalarValue.html
|
||||||
|
[`Executor`]: https://docs.rs/juniper/0.16.1/juniper/executor/struct.Executor.html
|
||||||
|
[`ScalarValue`]: https://docs.rs/juniper/0.16.1/juniper/trait.ScalarValue.html
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
[Rust]: https://www.rust-lang.org
|
||||||
|
[SQL]: https://en.wikipedia.org/wiki/SQL
|
||||||
|
|
||||||
|
[1]: https://spec.graphql.org/October2021#sec-Execution
|
||||||
|
[2]: https://spec.graphql.org/October2021#sec-Language.Operations\
|
||||||
|
[3]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||||
|
[20]: https://docs.rs/juniper/0.16.1/juniper/executor/struct.Executor.html#method.look_ahead
|
||||||
|
[21]: https://docs.rs/juniper/0.16.1/juniper/executor/struct.LookAheadSelection.html
|
||||||
|
[22]: https://docs.rs/juniper/0.16.1/juniper/executor/struct.LookAheadChildren.html
|
|
@ -1,73 +0,0 @@
|
||||||
# Multiple operations per request
|
|
||||||
|
|
||||||
The GraphQL standard generally assumes there will be one server request for each client operation you want to perform (such as a query or mutation). This is conceptually simple but has the potential to be inefficent.
|
|
||||||
|
|
||||||
Some client libraries such as [apollo-link-batch-http](https://www.apollographql.com/docs/link/links/batch-http.html) have added the ability to batch operations in a single HTTP request to save network round-trips and potentially increase performance. There are some [tradeoffs](https://blog.apollographql.com/batching-client-graphql-queries-a685f5bcd41b) that should be considered before batching requests.
|
|
||||||
|
|
||||||
Juniper's server integration crates support multiple operations in a single HTTP request using JSON arrays. This makes them compatible with client libraries that support batch operations without any special configuration.
|
|
||||||
|
|
||||||
Server integration crates maintained by others are **not required** to support batch requests. Batch requests aren't part of the official GraphQL specification.
|
|
||||||
|
|
||||||
Assuming an integration supports batch requests, for the following GraphQL query:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
{
|
|
||||||
hero {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The json data to POST to the server for an individual request would be:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"query": "{hero{name}}"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And the response would be of the form:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"hero": {
|
|
||||||
"name": "R2-D2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If you wanted to run the same query twice in a single HTTP request, the batched json data to POST to the server would be:
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"query": "{hero{name}}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"query": "{hero{name}}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
And the response would be of the form:
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"hero": {
|
|
||||||
"name": "R2-D2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"hero": {
|
|
||||||
"name": "R2-D2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
111
book/src/advanced/n_plus_1.md
Normal file
111
book/src/advanced/n_plus_1.md
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
N+1 problem
|
||||||
|
===========
|
||||||
|
|
||||||
|
A common issue with [GraphQL] server implementations is how the [resolvers][2] query their datasource. With a naive and straightforward approach we quickly run into the N+1 problem, resulting in a large number of unnecessary database queries or [HTTP] requests.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# extern crate anyhow;
|
||||||
|
# extern crate juniper;
|
||||||
|
# use anyhow::anyhow;
|
||||||
|
# use juniper::{graphql_object, GraphQLObject};
|
||||||
|
#
|
||||||
|
# type CultId = i32;
|
||||||
|
# type UserId = i32;
|
||||||
|
#
|
||||||
|
# struct Repository;
|
||||||
|
#
|
||||||
|
# impl juniper::Context for Repository {}
|
||||||
|
#
|
||||||
|
# impl Repository {
|
||||||
|
# async fn load_cult_by_id(&self, cult_id: CultId) -> anyhow::Result<Option<Cult>> { unimplemented!() }
|
||||||
|
# async fn load_all_persons(&self) -> anyhow::Result<Vec<Person>> { unimplemented!() }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
struct Cult {
|
||||||
|
id: CultId,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Person {
|
||||||
|
id: UserId,
|
||||||
|
name: String,
|
||||||
|
cult_id: CultId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
#[graphql(context = Repository)]
|
||||||
|
impl Person {
|
||||||
|
fn id(&self) -> CultId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
self.name.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cult(&self, #[graphql(ctx)] repo: &Repository) -> anyhow::Result<Cult> {
|
||||||
|
// Effectively performs the following SQL query:
|
||||||
|
// SELECT id, name FROM cults WHERE id = ${cult_id} LIMIT 1
|
||||||
|
repo.load_cult_by_id(self.cult_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("No cult exists for ID `{}`", self.cult_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Query;
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
#[graphql(context = Repository)]
|
||||||
|
impl Query {
|
||||||
|
async fn persons(#[graphql(ctx)] repo: &Repository) -> anyhow::Result<Vec<Person>> {
|
||||||
|
// Effectively performs the following SQL query:
|
||||||
|
// SELECT id, name, cult_id FROM persons
|
||||||
|
repo.load_all_persons().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's say we want to list a bunch of `cult`s `persons` were in:
|
||||||
|
```graphql
|
||||||
|
query {
|
||||||
|
persons {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
cult {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the `persons` [list][1] has been [resolved][2], a separate [SQL] query is run to find the `cult` of each `Person`. We can see how this could quickly become a problem.
|
||||||
|
```sql
|
||||||
|
SELECT id, name, cult_id FROM persons;
|
||||||
|
SELECT id, name FROM cults WHERE id = 1;
|
||||||
|
SELECT id, name FROM cults WHERE id = 2;
|
||||||
|
SELECT id, name FROM cults WHERE id = 1;
|
||||||
|
SELECT id, name FROM cults WHERE id = 3;
|
||||||
|
SELECT id, name FROM cults WHERE id = 4;
|
||||||
|
SELECT id, name FROM cults WHERE id = 1;
|
||||||
|
SELECT id, name FROM cults WHERE id = 2;
|
||||||
|
-- and so on...
|
||||||
|
```
|
||||||
|
|
||||||
|
There are several ways how this problem may be resolved in [Juniper]. The most common ones are:
|
||||||
|
- [DataLoader](dataloader.md)
|
||||||
|
- [Look-ahead machinery](lookahead.md)
|
||||||
|
- [Eager loading](eager_loading.md)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
[HTTP]: https://en.wikipedia.org/wiki/HTTP
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
[Rust]: https://www.rust-lang.org
|
||||||
|
[SQL]: https://en.wikipedia.org/wiki/SQL
|
||||||
|
|
||||||
|
[1]: https://spec.graphql.org/October2021#sec-List
|
||||||
|
[2]: https://spec.graphql.org/October2021#sec-Executing-Fields
|
|
@ -1,58 +0,0 @@
|
||||||
# Non-struct objects
|
|
||||||
|
|
||||||
Up until now, we've only looked at mapping structs to GraphQL objects. However,
|
|
||||||
any Rust type can be mapped into a GraphQL object. In this chapter, we'll look
|
|
||||||
at enums, but traits will work too - they don't _have_ to be mapped into GraphQL
|
|
||||||
interfaces.
|
|
||||||
|
|
||||||
Using `Result`-like enums can be a useful way of reporting e.g. validation
|
|
||||||
errors from a mutation:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::{graphql_object, GraphQLObject};
|
|
||||||
# #[derive(juniper::GraphQLObject)] struct User { name: String }
|
|
||||||
#
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
struct ValidationError {
|
|
||||||
field: String,
|
|
||||||
message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
# #[allow(dead_code)]
|
|
||||||
enum SignUpResult {
|
|
||||||
Ok(User),
|
|
||||||
Error(Vec<ValidationError>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[graphql_object]
|
|
||||||
impl SignUpResult {
|
|
||||||
fn user(&self) -> Option<&User> {
|
|
||||||
match *self {
|
|
||||||
SignUpResult::Ok(ref user) => Some(user),
|
|
||||||
SignUpResult::Error(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn error(&self) -> Option<&Vec<ValidationError>> {
|
|
||||||
match *self {
|
|
||||||
SignUpResult::Ok(_) => None,
|
|
||||||
SignUpResult::Error(ref errors) => Some(errors)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
Here, we use an enum to decide whether a user's input data was valid or not, and
|
|
||||||
it could be used as the result of e.g. a sign up mutation.
|
|
||||||
|
|
||||||
While this is an example of how you could use something other than a struct to
|
|
||||||
represent a GraphQL object, it's also an example on how you could implement
|
|
||||||
error handling for "expected" errors - errors like validation errors. There are
|
|
||||||
no hard rules on how to represent errors in GraphQL, but there are
|
|
||||||
[some](https://github.com/facebook/graphql/issues/117#issuecomment-170180628)
|
|
||||||
[comments](https://github.com/graphql/graphql-js/issues/560#issuecomment-259508214)
|
|
||||||
from one of the authors of GraphQL on how they intended "hard" field errors to
|
|
||||||
be used, and how to model expected errors.
|
|
|
@ -1,66 +0,0 @@
|
||||||
# Objects and generics
|
|
||||||
|
|
||||||
Yet another point where GraphQL and Rust differs is in how generics work. In
|
|
||||||
Rust, almost any type could be generic - that is, take type parameters. In
|
|
||||||
GraphQL, there are only two generic types: lists and non-nullables.
|
|
||||||
|
|
||||||
This poses a restriction on what you can expose in GraphQL from Rust: no generic
|
|
||||||
structs can be exposed - all type parameters must be bound. For example, you can
|
|
||||||
not make e.g. `Result<T, E>` into a GraphQL type, but you _can_ make e.g.
|
|
||||||
`Result<User, String>` into a GraphQL type.
|
|
||||||
|
|
||||||
Let's make a slightly more compact but generic implementation of [the last
|
|
||||||
chapter](non_struct_objects.md):
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# #[derive(juniper::GraphQLObject)] struct User { name: String }
|
|
||||||
# #[derive(juniper::GraphQLObject)] struct ForumPost { title: String }
|
|
||||||
|
|
||||||
#[derive(juniper::GraphQLObject)]
|
|
||||||
struct ValidationError {
|
|
||||||
field: String,
|
|
||||||
message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
# #[allow(dead_code)]
|
|
||||||
struct MutationResult<T>(Result<T, Vec<ValidationError>>);
|
|
||||||
|
|
||||||
#[juniper::graphql_object(
|
|
||||||
name = "UserResult",
|
|
||||||
)]
|
|
||||||
impl MutationResult<User> {
|
|
||||||
fn user(&self) -> Option<&User> {
|
|
||||||
self.0.as_ref().ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn error(&self) -> Option<&Vec<ValidationError>> {
|
|
||||||
self.0.as_ref().err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[juniper::graphql_object(
|
|
||||||
name = "ForumPostResult",
|
|
||||||
)]
|
|
||||||
impl MutationResult<ForumPost> {
|
|
||||||
fn forum_post(&self) -> Option<&ForumPost> {
|
|
||||||
self.0.as_ref().ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn error(&self) -> Option<&Vec<ValidationError>> {
|
|
||||||
self.0.as_ref().err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
Here, we've made a wrapper around `Result` and exposed some concrete
|
|
||||||
instantiations of `Result<T, E>` as distinct GraphQL objects. The reason we
|
|
||||||
needed the wrapper is of Rust's rules for when you can derive a trait - in this
|
|
||||||
case, both `Result` and Juniper's internal GraphQL trait are from third-party
|
|
||||||
sources.
|
|
||||||
|
|
||||||
Because we're using generics, we also need to specify a name for our
|
|
||||||
instantiated types. Even if Juniper _could_ figure out the name,
|
|
||||||
`MutationResult<User>` wouldn't be a valid GraphQL type name.
|
|
|
@ -1,175 +0,0 @@
|
||||||
# Subscriptions
|
|
||||||
### How to achieve realtime data with GraphQL subscriptions
|
|
||||||
|
|
||||||
GraphQL subscriptions are a way to push data from the server to clients requesting real-time messages
|
|
||||||
from the server. Subscriptions are similar to queries in that they specify a set of fields to be delivered to the client,
|
|
||||||
but instead of immediately returning a single answer a result is sent every time a particular event happens on the
|
|
||||||
server.
|
|
||||||
|
|
||||||
In order to execute subscriptions you need a coordinator (that spawns connections)
|
|
||||||
and a GraphQL object that can be resolved into a stream--elements of which will then
|
|
||||||
be returned to the end user. The [`juniper_subscriptions`][juniper_subscriptions] crate
|
|
||||||
provides a default connection implementation. Currently subscriptions are only supported on the `master` branch. Add the following to your `Cargo.toml`:
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
juniper = "0.16.0"
|
|
||||||
juniper_subscriptions = "0.17.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Schema Definition
|
|
||||||
|
|
||||||
The `Subscription` is just a GraphQL object, similar to the query root and mutations object that you defined for the
|
|
||||||
operations in your [Schema][Schema]. For subscriptions all fields/operations should be async and should return a [Stream][Stream].
|
|
||||||
|
|
||||||
This example shows a subscription operation that returns two events, the strings `Hello` and `World!`
|
|
||||||
sequentially:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate futures;
|
|
||||||
# extern crate juniper;
|
|
||||||
# use std::pin::Pin;
|
|
||||||
# use futures::Stream;
|
|
||||||
# use juniper::{graphql_object, graphql_subscription, FieldError};
|
|
||||||
#
|
|
||||||
# #[derive(Clone)]
|
|
||||||
# pub struct Database;
|
|
||||||
# impl juniper::Context for Database {}
|
|
||||||
|
|
||||||
# pub struct Query;
|
|
||||||
# #[graphql_object(context = Database)]
|
|
||||||
# impl Query {
|
|
||||||
# fn hello_world() -> &'static str {
|
|
||||||
# "Hello World!"
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
pub struct Subscription;
|
|
||||||
|
|
||||||
type StringStream = Pin<Box<dyn Stream<Item = Result<String, FieldError>> + Send>>;
|
|
||||||
|
|
||||||
#[graphql_subscription(context = Database)]
|
|
||||||
impl Subscription {
|
|
||||||
async fn hello_world() -> StringStream {
|
|
||||||
let stream = futures::stream::iter(vec![
|
|
||||||
Ok(String::from("Hello")),
|
|
||||||
Ok(String::from("World!"))
|
|
||||||
]);
|
|
||||||
Box::pin(stream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main () {}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Coordinator
|
|
||||||
|
|
||||||
Subscriptions require a bit more resources than regular queries and provide a great vector for DOS attacks. This can can bring down a server easily if not handled correctly. The [`SubscriptionCoordinator`][SubscriptionCoordinator] trait provides coordination logic to enable functionality like DOS attack mitigation and resource limits.
|
|
||||||
|
|
||||||
The [`SubscriptionCoordinator`][SubscriptionCoordinator] contains the schema and can keep track of opened connections, handle subscription
|
|
||||||
start and end, and maintain a global subscription id for each subscription. Each time a connection is established,
|
|
||||||
the [`SubscriptionCoordinator`][SubscriptionCoordinator] spawns a [`SubscriptionConnection`][SubscriptionConnection]. The [`SubscriptionConnection`][SubscriptionConnection] handles a single connection, providing resolver logic for a client stream as well as reconnection
|
|
||||||
and shutdown logic.
|
|
||||||
|
|
||||||
|
|
||||||
While you can implement [`SubscriptionCoordinator`][SubscriptionCoordinator] yourself, Juniper contains a simple and generic implementation called [`Coordinator`][Coordinator]. The `subscribe`
|
|
||||||
operation returns a [`Future`][Future] with an `Item` value of a `Result<Connection, GraphQLError>`,
|
|
||||||
where [`Connection`][Connection] is a `Stream` of values returned by the operation and [`GraphQLError`][GraphQLError] is the error when the subscription fails.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# #![allow(dead_code)]
|
|
||||||
# extern crate futures;
|
|
||||||
# extern crate juniper;
|
|
||||||
# extern crate juniper_subscriptions;
|
|
||||||
# extern crate serde_json;
|
|
||||||
# use juniper::{
|
|
||||||
# http::GraphQLRequest,
|
|
||||||
# graphql_object, graphql_subscription,
|
|
||||||
# DefaultScalarValue, EmptyMutation, FieldError,
|
|
||||||
# RootNode, SubscriptionCoordinator,
|
|
||||||
# };
|
|
||||||
# use juniper_subscriptions::Coordinator;
|
|
||||||
# use futures::{Stream, StreamExt};
|
|
||||||
# use std::pin::Pin;
|
|
||||||
#
|
|
||||||
# #[derive(Clone)]
|
|
||||||
# pub struct Database;
|
|
||||||
#
|
|
||||||
# impl juniper::Context for Database {}
|
|
||||||
#
|
|
||||||
# impl Database {
|
|
||||||
# fn new() -> Self {
|
|
||||||
# Self
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# pub struct Query;
|
|
||||||
#
|
|
||||||
# #[graphql_object(context = Database)]
|
|
||||||
# impl Query {
|
|
||||||
# fn hello_world() -> &'static str {
|
|
||||||
# "Hello World!"
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# pub struct Subscription;
|
|
||||||
#
|
|
||||||
# type StringStream = Pin<Box<dyn Stream<Item = Result<String, FieldError>> + Send>>;
|
|
||||||
#
|
|
||||||
# #[graphql_subscription(context = Database)]
|
|
||||||
# impl Subscription {
|
|
||||||
# async fn hello_world() -> StringStream {
|
|
||||||
# let stream =
|
|
||||||
# futures::stream::iter(vec![Ok(String::from("Hello")), Ok(String::from("World!"))]);
|
|
||||||
# Box::pin(stream)
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
type Schema = RootNode<'static, Query, EmptyMutation<Database>, Subscription>;
|
|
||||||
|
|
||||||
fn schema() -> Schema {
|
|
||||||
Schema::new(Query, EmptyMutation::new(), Subscription)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_subscription() {
|
|
||||||
let schema = schema();
|
|
||||||
let coordinator = Coordinator::new(schema);
|
|
||||||
let req: GraphQLRequest<DefaultScalarValue> = serde_json::from_str(
|
|
||||||
r#"{
|
|
||||||
"query": "subscription { helloWorld }"
|
|
||||||
}"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let ctx = Database::new();
|
|
||||||
let mut conn = coordinator.subscribe(&req, &ctx).await.unwrap();
|
|
||||||
while let Some(result) = conn.next().await {
|
|
||||||
println!("{}", serde_json::to_string(&result).unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() { }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Web Integration and Examples
|
|
||||||
|
|
||||||
Currently there is an example of subscriptions with [warp][warp], but it still in an alpha state.
|
|
||||||
GraphQL over [WS][WS] is not fully supported yet and is non-standard.
|
|
||||||
|
|
||||||
- [Warp Subscription Example](https://github.com/graphql-rust/juniper/tree/master/examples/warp_subscriptions)
|
|
||||||
- [Small Example](https://github.com/graphql-rust/juniper/tree/master/examples/basic_subscriptions)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[juniper_subscriptions]: https://github.com/graphql-rust/juniper/tree/master/juniper_subscriptions
|
|
||||||
[Stream]: https://docs.rs/futures/0.3.4/futures/stream/trait.Stream.html
|
|
||||||
<!-- TODO: Fix these links when the documentation for the `juniper_subscriptions` are defined in the docs. --->
|
|
||||||
[Coordinator]: https://docs.rs/juniper_subscriptions/0.15.0/struct.Coordinator.html
|
|
||||||
[SubscriptionCoordinator]: https://docs.rs/juniper_subscriptions/0.15.0/trait.SubscriptionCoordinator.html
|
|
||||||
[Connection]: https://docs.rs/juniper_subscriptions/0.15.0/struct.Connection.html
|
|
||||||
[SubscriptionConnection]: https://docs.rs/juniper_subscriptions/0.15.0/trait.SubscriptionConnection.html
|
|
||||||
<!--- --->
|
|
||||||
[Future]: https://docs.rs/futures/0.3.4/futures/future/trait.Future.html
|
|
||||||
[warp]: https://github.com/graphql-rust/juniper/tree/master/juniper_warp
|
|
||||||
[WS]: https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md
|
|
||||||
[GraphQLError]: https://docs.rs/juniper/0.14.2/juniper/enum.GraphQLError.html
|
|
||||||
[Schema]: ../schema/schemas_and_mutations.md
|
|
85
book/src/introduction.md
Normal file
85
book/src/introduction.md
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
Introduction
|
||||||
|
============
|
||||||
|
|
||||||
|
> [GraphQL] is a query language for APIs and a runtime for fulfilling those queries with your existing data. [GraphQL] provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.
|
||||||
|
|
||||||
|
[Juniper] is a library for creating [GraphQL] servers in [Rust]. Build type-safe and fast API servers with minimal boilerplate and configuration (we do try to make declaring and resolving [GraphQL] schemas as convenient as [Rust] will allow).
|
||||||
|
|
||||||
|
[Juniper] doesn't include a web server itself, instead, it provides building blocks to make integration with existing web servers straightforward. It optionally provides a pre-built integration for some widely used web server frameworks in [Rust] ecosystem.
|
||||||
|
|
||||||
|
- [Cargo crate](https://crates.io/crates/juniper)
|
||||||
|
- [API reference][`juniper`]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
[Juniper] supports the full GraphQL query language according to the [specification (October 2021)][GraphQL spec].
|
||||||
|
|
||||||
|
> **NOTE**: As an exception to other [GraphQL] libraries for other languages, [Juniper] builds non-`null` types by default. A field of type `Vec<Episode>` will be converted into `[Episode!]!`. The corresponding Rust type for a `null`able `[Episode]` would be `Option<Vec<Option<Episode>>>` instead.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Integrations
|
||||||
|
|
||||||
|
|
||||||
|
### Types
|
||||||
|
|
||||||
|
[Juniper] provides out-of-the-box integration for some very common [Rust] crates to make building schemas a breeze. The types from these crates will be usable in your schemas automatically after enabling the correspondent self-titled [Cargo feature]:
|
||||||
|
- [`bigdecimal`]
|
||||||
|
- [`bson`]
|
||||||
|
- [`chrono`], [`chrono-tz`]
|
||||||
|
- [`jiff`]
|
||||||
|
- [`rust_decimal`]
|
||||||
|
- [`time`]
|
||||||
|
- [`url`]
|
||||||
|
- [`uuid`]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Web server frameworks
|
||||||
|
|
||||||
|
- [`actix-web`] ([`juniper_actix`] crate)
|
||||||
|
- [`axum`] ([`juniper_axum`] crate)
|
||||||
|
- [`hyper`] ([`juniper_hyper`] crate)
|
||||||
|
- [`rocket`] ([`juniper_rocket`] crate)
|
||||||
|
- [`warp`] ([`juniper_warp`] crate)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## API stability
|
||||||
|
|
||||||
|
[Juniper] has not reached 1.0 yet, thus some API instability should be expected.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[`actix-web`]: https://docs.rs/actix-web
|
||||||
|
[`axum`]: https://docs.rs/axum
|
||||||
|
[`bigdecimal`]: https://docs.rs/bigdecimal
|
||||||
|
[`bson`]: https://docs.rs/bson
|
||||||
|
[`chrono`]: https://docs.rs/chrono
|
||||||
|
[`chrono-tz`]: https://docs.rs/chrono-tz
|
||||||
|
[`jiff`]: https://docs.rs/jiff
|
||||||
|
[`juniper`]: https://docs.rs/juniper
|
||||||
|
[`juniper_actix`]: https://docs.rs/juniper_actix
|
||||||
|
[`juniper_axum`]: https://docs.rs/juniper_axum
|
||||||
|
[`juniper_hyper`]: https://docs.rs/juniper_hyper
|
||||||
|
[`juniper_rocket`]: https://docs.rs/juniper_rocket
|
||||||
|
[`juniper_warp`]: https://docs.rs/juniper_warp
|
||||||
|
[`hyper`]: https://docs.rs/hyper
|
||||||
|
[`rocket`]: https://docs.rs/rocket
|
||||||
|
[`rust_decimal`]: https://docs.rs/rust_decimal
|
||||||
|
[`time`]: https://docs.rs/time
|
||||||
|
[`url`]: https://docs.rs/url
|
||||||
|
[`uuid`]: https://docs.rs/uuid
|
||||||
|
[`warp`]: https://docs.rs/warp
|
||||||
|
[Cargo feature]: https://doc.rust-lang.org/cargo/reference/features.html
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
[GraphQL spec]: https://spec.graphql.org/October2021
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
[Rust]: https://www.rust-lang.org
|
|
@ -1,34 +1,39 @@
|
||||||
# Quickstart
|
Quickstart
|
||||||
|
==========
|
||||||
|
|
||||||
|
This page will give you a short introduction to the concepts in [Juniper].
|
||||||
|
|
||||||
|
**[Juniper] follows a [code-first] approach to define a [GraphQL] schema.**
|
||||||
|
|
||||||
|
> **TIP**: For a [schema-first] approach, consider using a [`juniper-from-schema`] crate for generating a [`juniper`]-based code from a [schema] file.
|
||||||
|
|
||||||
|
|
||||||
This page will give you a short introduction to the concepts in Juniper.
|
|
||||||
|
|
||||||
Juniper follows a [code-first approach][schema_approach] to defining GraphQL schemas. If you would like to use a [schema-first approach][schema_approach] instead, consider [juniper-from-schema][] for generating code from a schema file.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
juniper = "0.16.0"
|
juniper = "0.16.1"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Schema example
|
|
||||||
|
|
||||||
Exposing simple enums and structs as GraphQL is just a matter of adding a custom
|
|
||||||
derive attribute to them. Juniper includes support for basic Rust types that
|
|
||||||
naturally map to GraphQL features, such as `Option<T>`, `Vec<T>`, `Box<T>`,
|
|
||||||
`String`, `f64`, and `i32`, references, and slices.
|
|
||||||
|
|
||||||
For more advanced mappings, Juniper provides multiple macros to map your Rust
|
|
||||||
types to a GraphQL schema. The most important one is the
|
## Schema
|
||||||
[graphql_object][graphql_object] procedural macro that is used for declaring an object with
|
|
||||||
resolvers, which you will use for the `Query` and `Mutation` roots.
|
Exposing simple enums and structs as [GraphQL] types is just a matter of adding a custom [derive attribute] to them. [Juniper] includes support for basic [Rust] types that naturally map to [GraphQL] features, such as `Option<T>`, `Vec<T>`, `Box<T>`, `Arc<T>`, `String`, `f64`, `i32`, references, slices and arrays.
|
||||||
|
|
||||||
|
For more advanced mappings, [Juniper] provides multiple macros to map your [Rust] types to a [GraphQL schema][schema]. The most important one is the [`#[graphql_object]` attribute][2] that is used for declaring a [GraphQL object] with resolvers (typically used for declaring [`Query` and `Mutation` roots][1]).
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# #![allow(unused_variables)]
|
# # ![allow(unused_variables)]
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
|
#
|
||||||
# use std::fmt::Display;
|
# use std::fmt::Display;
|
||||||
|
#
|
||||||
use juniper::{
|
use juniper::{
|
||||||
graphql_object, EmptySubscription, FieldResult, GraphQLEnum,
|
graphql_object, EmptySubscription, FieldResult, GraphQLEnum,
|
||||||
GraphQLInputObject, GraphQLObject, ScalarValue,
|
GraphQLInputObject, GraphQLObject, ScalarValue,
|
||||||
};
|
};
|
||||||
#
|
#
|
||||||
|
@ -56,7 +61,6 @@ struct Human {
|
||||||
}
|
}
|
||||||
|
|
||||||
// There is also a custom derive for mapping GraphQL input objects.
|
// There is also a custom derive for mapping GraphQL input objects.
|
||||||
|
|
||||||
#[derive(GraphQLInputObject)]
|
#[derive(GraphQLInputObject)]
|
||||||
#[graphql(description = "A humanoid creature in the Star Wars universe")]
|
#[graphql(description = "A humanoid creature in the Star Wars universe")]
|
||||||
struct NewHuman {
|
struct NewHuman {
|
||||||
|
@ -65,52 +69,58 @@ struct NewHuman {
|
||||||
home_planet: String,
|
home_planet: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now, we create our root Query and Mutation types with resolvers by using the
|
// Now, we create our root `Query` and `Mutation` types with resolvers by using
|
||||||
// object macro.
|
// the `#[graphql_object]` attribute.
|
||||||
// Objects can have contexts that allow accessing shared state like a database
|
|
||||||
// pool.
|
|
||||||
|
|
||||||
|
// Resolvers can have a context that allows accessing shared state like a
|
||||||
|
// database pool.
|
||||||
struct Context {
|
struct Context {
|
||||||
// Use your real database pool here.
|
// Use your real database pool here.
|
||||||
pool: DatabasePool,
|
db: DatabasePool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// To make our context usable by Juniper, we have to implement a marker trait.
|
// To make our `Context` usable by `juniper`, we have to implement a marker
|
||||||
|
// trait.
|
||||||
impl juniper::Context for Context {}
|
impl juniper::Context for Context {}
|
||||||
|
|
||||||
struct Query;
|
struct Query;
|
||||||
|
|
||||||
#[graphql_object(
|
// Here we specify the context type for the object.
|
||||||
// Here we specify the context type for the object.
|
// We need to do this in every type that needs access to the `Context`.
|
||||||
// We need to do this in every type that
|
#[graphql_object]
|
||||||
// needs access to the context.
|
#[graphql(context = Context)]
|
||||||
context = Context,
|
|
||||||
)]
|
|
||||||
impl Query {
|
impl Query {
|
||||||
fn apiVersion() -> &'static str {
|
// Note, that the field name will be automatically converted to the
|
||||||
|
// `camelCased` variant, just as GraphQL conventions imply.
|
||||||
|
fn api_version() -> &'static str {
|
||||||
"1.0"
|
"1.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arguments to resolvers can either be simple types or input objects.
|
fn human(
|
||||||
// To gain access to the context, we specify a argument
|
// Arguments to resolvers can either be simple scalar types, enums or
|
||||||
// that is a reference to the Context type.
|
// input objects.
|
||||||
// Juniper automatically injects the correct context here.
|
id: String,
|
||||||
fn human(context: &Context, id: String) -> FieldResult<Human> {
|
// To gain access to the `Context`, we specify a `context`-named
|
||||||
// Get a db connection.
|
// argument referring the correspondent `Context` type, and `juniper`
|
||||||
let connection = context.pool.get_connection()?;
|
// will inject it automatically.
|
||||||
// Execute a db query.
|
context: &Context,
|
||||||
|
) -> FieldResult<Human> {
|
||||||
|
// Get a `db` connection.
|
||||||
|
let conn = context.db.get_connection()?;
|
||||||
|
// Execute a `db` query.
|
||||||
// Note the use of `?` to propagate errors.
|
// Note the use of `?` to propagate errors.
|
||||||
let human = connection.find_human(&id)?;
|
let human = conn.find_human(&id)?;
|
||||||
// Return the result.
|
// Return the result.
|
||||||
Ok(human)
|
Ok(human)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now, we do the same for our Mutation type.
|
// Now, we do the same for our `Mutation` type.
|
||||||
|
|
||||||
struct Mutation;
|
struct Mutation;
|
||||||
|
|
||||||
#[graphql_object(
|
#[graphql_object]
|
||||||
|
#[graphql(
|
||||||
context = Context,
|
context = Context,
|
||||||
// If we need to use `ScalarValue` parametrization explicitly somewhere
|
// If we need to use `ScalarValue` parametrization explicitly somewhere
|
||||||
// in the object definition (like here in `FieldResult`), we could
|
// in the object definition (like here in `FieldResult`), we could
|
||||||
|
@ -118,42 +128,48 @@ struct Mutation;
|
||||||
scalar = S: ScalarValue + Display,
|
scalar = S: ScalarValue + Display,
|
||||||
)]
|
)]
|
||||||
impl Mutation {
|
impl Mutation {
|
||||||
fn createHuman<S: ScalarValue + Display>(context: &Context, new_human: NewHuman) -> FieldResult<Human, S> {
|
fn create_human<S: ScalarValue + Display>(
|
||||||
let db = context.pool.get_connection().map_err(|e| e.map_scalar_value())?;
|
new_human: NewHuman,
|
||||||
|
context: &Context,
|
||||||
|
) -> FieldResult<Human, S> {
|
||||||
|
let db = context.db.get_connection().map_err(|e| e.map_scalar_value())?;
|
||||||
let human: Human = db.insert_human(&new_human).map_err(|e| e.map_scalar_value())?;
|
let human: Human = db.insert_human(&new_human).map_err(|e| e.map_scalar_value())?;
|
||||||
Ok(human)
|
Ok(human)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A root schema consists of a query, a mutation, and a subscription.
|
// Root schema consists of a query, a mutation, and a subscription.
|
||||||
// Request queries can be executed against a RootNode.
|
// Request queries can be executed against a `RootNode`.
|
||||||
type Schema = juniper::RootNode<'static, Query, Mutation, EmptySubscription<Context>>;
|
type Schema = juniper::RootNode<'static, Query, Mutation, EmptySubscription<Context>>;
|
||||||
#
|
#
|
||||||
# fn main() {
|
# fn main() {
|
||||||
# let _ = Schema::new(Query, Mutation, EmptySubscription::new());
|
# _ = Schema::new(Query, Mutation, EmptySubscription::new());
|
||||||
# }
|
# }
|
||||||
```
|
```
|
||||||
|
|
||||||
We now have a very simple but functional schema for a GraphQL server!
|
Now we have a very simple but functional schema for a [GraphQL] server!
|
||||||
|
|
||||||
To actually serve the schema, see the guides for our various [server integrations](./servers/index.md).
|
To actually serve the [schema], see the guides for our various [server integrations](serve/index.md).
|
||||||
|
|
||||||
Juniper is a library that can be used in many contexts--it does not require a server and it does not have a dependency on a particular transport or serialization format. You can invoke the executor directly to get a result for a query:
|
|
||||||
|
|
||||||
## Executor
|
|
||||||
|
|
||||||
You can invoke `juniper::execute` directly to run a GraphQL query:
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
[Juniper] is a library that can be used in many contexts: it doesn't require a server, nor it has a dependency on a particular transport or serialization format. You can invoke the `juniper::execute()` directly to get a result for a [GraphQL] query:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# // Only needed due to 2018 edition because the macro is not accessible.
|
# // Only needed due to 2018 edition because the macro is not accessible.
|
||||||
# #[macro_use] extern crate juniper;
|
# #[macro_use] extern crate juniper;
|
||||||
use juniper::{
|
use juniper::{
|
||||||
graphql_object, EmptyMutation, EmptySubscription, FieldResult,
|
graphql_object, graphql_value, EmptyMutation, EmptySubscription,
|
||||||
GraphQLEnum, Variables, graphql_value,
|
GraphQLEnum, Variables,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(GraphQLEnum, Clone, Copy)]
|
#[derive(GraphQLEnum, Clone, Copy)]
|
||||||
enum Episode {
|
enum Episode {
|
||||||
|
// Note, that the enum value will be automatically converted to the
|
||||||
|
// `SCREAMING_SNAKE_CASE` variant, just as GraphQL conventions imply.
|
||||||
NewHope,
|
NewHope,
|
||||||
Empire,
|
Empire,
|
||||||
Jedi,
|
Jedi,
|
||||||
|
@ -166,22 +182,21 @@ impl juniper::Context for Ctx {}
|
||||||
|
|
||||||
struct Query;
|
struct Query;
|
||||||
|
|
||||||
#[graphql_object(context = Ctx)]
|
#[graphql_object]
|
||||||
|
#[graphql(context = Ctx)]
|
||||||
impl Query {
|
impl Query {
|
||||||
fn favoriteEpisode(context: &Ctx) -> FieldResult<Episode> {
|
fn favorite_episode(context: &Ctx) -> Episode {
|
||||||
Ok(context.0)
|
context.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A root schema consists of a query, a mutation, and a subscription.
|
|
||||||
// Request queries can be executed against a RootNode.
|
|
||||||
type Schema = juniper::RootNode<'static, Query, EmptyMutation<Ctx>, EmptySubscription<Ctx>>;
|
type Schema = juniper::RootNode<'static, Query, EmptyMutation<Ctx>, EmptySubscription<Ctx>>;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// Create a context object.
|
// Create a context.
|
||||||
let ctx = Ctx(Episode::NewHope);
|
let ctx = Ctx(Episode::NewHope);
|
||||||
|
|
||||||
// Run the executor.
|
// Run the execution.
|
||||||
let (res, _errors) = juniper::execute_sync(
|
let (res, _errors) = juniper::execute_sync(
|
||||||
"query { favoriteEpisode }",
|
"query { favoriteEpisode }",
|
||||||
None,
|
None,
|
||||||
|
@ -190,21 +205,28 @@ fn main() {
|
||||||
&ctx,
|
&ctx,
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
// Ensure the value matches.
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
res,
|
res,
|
||||||
graphql_value!({
|
graphql_value!({
|
||||||
"favoriteEpisode": "NEW_HOPE",
|
"favoriteEpisode": "NEW_HOPE",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
[juniper-from-schema]: https://github.com/davidpdrsn/juniper-from-schema
|
|
||||||
[schema_approach]: https://blog.logrocket.com/code-first-vs-schema-first-development-graphql/
|
|
||||||
[hyper]: servers/hyper.md
|
|
||||||
[warp]: servers/warp.md
|
[`juniper`]: https://docs.rs/juniper
|
||||||
[rocket]: servers/rocket.md
|
[`juniper-from-schema`]: https://docs.rs/juniper-from-schema
|
||||||
[iron]: servers/iron.md
|
[code-first]: https://www.apollographql.com/blog/backend/architecture/schema-first-vs-code-only-graphql#code-only
|
||||||
[tutorial]: ./tutorial.html
|
[derive attribute]: https://doc.rust-lang.org/stable/reference/attributes/derive.html#derive
|
||||||
[graphql_object]: https://docs.rs/juniper/latest/juniper/macro.graphql_object.html
|
[GraphQL]: https://graphql.org
|
||||||
|
[GraphQL object]: https://spec.graphql.org/October2021#sec-Objects
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
[Rust]: https://www.rust-lang.org
|
||||||
|
[schema]: https://graphql.org/learn/schema
|
||||||
|
[schema-first]: https://www.apollographql.com/blog/backend/architecture/schema-first-vs-code-only-graphql#schema-first
|
||||||
|
|
||||||
|
[1]: https://spec.graphql.org/October2021#sec-Root-Operation-Types
|
||||||
|
[2]: https://docs.rs/juniper/0.16.1/juniper/macro.graphql_object.html
|
||||||
|
|
185
book/src/schema/index.md
Normal file
185
book/src/schema/index.md
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
Schema
|
||||||
|
======
|
||||||
|
|
||||||
|
**[Juniper] follows a [code-first] approach to define a [GraphQL] schema.**
|
||||||
|
|
||||||
|
> **TIP**: For a [schema-first] approach, consider using a [`juniper-from-schema`] crate for generating a [`juniper`]-based code from a [schema] file.
|
||||||
|
|
||||||
|
[GraphQL schema][0] consists of three [object types][4]: a [query root][1], a [mutation root][2], and a [subscription root][3].
|
||||||
|
|
||||||
|
> The **query** root operation type must be provided and must be an [Object][4] type.
|
||||||
|
>
|
||||||
|
> The **mutation** root operation type is optional; if it is not provided, the service does not support mutations. If it is provided, it must be an [Object][4] type.
|
||||||
|
>
|
||||||
|
> Similarly, the **subscription** root operation type is also optional; if it is not provided, the service does not support subscriptions. If it is provided, it must be an [Object][4] type.
|
||||||
|
>
|
||||||
|
> The **query**, **mutation**, and **subscription** root types must all be different types if provided.
|
||||||
|
|
||||||
|
In [Juniper], the [`RootNode`] type represents a [schema][0]. When the [schema][0] is first created, [Juniper] will traverse the entire object graph and register all types it can find. This means that if we [define a GraphQL object](../types/objects/index.md) somewhere but never use or reference it, it won't be exposed in a [GraphQL schema][0].
|
||||||
|
|
||||||
|
Both [query][1] and [mutation][2] objects are regular [GraphQL objects][4], defined like [any other object in Juniper](../types/objects/index.md). The [mutation][2] and [subscription][3] objects, however, are optional, since [schemas][0] can be read-only and do not require [subscriptions][3].
|
||||||
|
|
||||||
|
> **TIP**: If [mutation][2]/[subscription][3] functionality is not needed, consider using the predefined [`EmptyMutation`]/[`EmptySubscription`] types for stubbing them in a [`RootNode`].
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::{
|
||||||
|
# graphql_object, EmptySubscription, FieldResult, GraphQLObject, RootNode,
|
||||||
|
# };
|
||||||
|
#
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
struct User {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Query;
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
impl Query {
|
||||||
|
fn user_with_username(username: String) -> FieldResult<Option<User>> {
|
||||||
|
// Look up user in database...
|
||||||
|
# unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Mutation;
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
impl Mutation {
|
||||||
|
fn sign_up_user(name: String, email: String) -> FieldResult<User> {
|
||||||
|
// Validate inputs and save user in database...
|
||||||
|
# unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Schema = RootNode<'static, Query, Mutation, EmptySubscription>;
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **NOTE**: It's considered a [good practice][5] to name [query][1], [mutation][2], and [subscription][3] root types as `Query`, `Mutation`, and `Subscription` respectively.
|
||||||
|
|
||||||
|
The usage of [subscriptions][3] is a little different from the [mutation][2] and [query][1] [objects][4], so they are discussed in the [separate chapter](subscriptions.md).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Export
|
||||||
|
|
||||||
|
Many tools in [GraphQL] ecosystem require a [schema] definition to operate on. With [Juniper] we can export our [GraphQL schema][0] defined in [Rust] code either represented in the [GraphQL schema language][6] or in [JSON].
|
||||||
|
|
||||||
|
|
||||||
|
### SDL (schema definition language)
|
||||||
|
|
||||||
|
To generate an [SDL (schema definition language)][6] representation of a [GraphQL schema][0] defined in [Rust] code, the [`as_sdl()` method][20] should be used for the direct extraction (requires enabling the `schema-language` [Juniper] feature):
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::{
|
||||||
|
# graphql_object, EmptyMutation, EmptySubscription, FieldResult, RootNode,
|
||||||
|
# };
|
||||||
|
#
|
||||||
|
struct Query;
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
impl Query {
|
||||||
|
fn hello(&self) -> FieldResult<&str> {
|
||||||
|
Ok("hello world")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Define our schema in Rust.
|
||||||
|
let schema = RootNode::new(
|
||||||
|
Query,
|
||||||
|
EmptyMutation::<()>::new(),
|
||||||
|
EmptySubscription::<()>::new(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert the Rust schema into the GraphQL SDL schema.
|
||||||
|
let result = schema.as_sdl();
|
||||||
|
|
||||||
|
let expected = "\
|
||||||
|
schema {
|
||||||
|
query: Query
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
hello: String!
|
||||||
|
}
|
||||||
|
";
|
||||||
|
# #[cfg(not(target_os = "windows"))]
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### JSON
|
||||||
|
|
||||||
|
To export a [GraphQL schema][0] defined in [Rust] code as [JSON] (often referred to as `schema.json`), the specially crafted [introspection query][21] should be issued. [Juniper] provides a [convenience `introspect()` function][22] to [introspect](introspection.md) the entire [schema][0], which result can be serialized into [JSON]:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# extern crate serde_json;
|
||||||
|
# use juniper::{
|
||||||
|
# graphql_object, EmptyMutation, EmptySubscription, GraphQLObject,
|
||||||
|
# IntrospectionFormat, RootNode,
|
||||||
|
# };
|
||||||
|
#
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
struct Example {
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Query;
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
impl Query {
|
||||||
|
fn example(id: String) -> Example {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Run the built-in introspection query.
|
||||||
|
let (res, _errors) = juniper::introspect(
|
||||||
|
&Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()),
|
||||||
|
&(),
|
||||||
|
IntrospectionFormat::default(),
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
// Serialize the introspection result into JSON.
|
||||||
|
let json_result = serde_json::to_string_pretty(&res);
|
||||||
|
assert!(json_result.is_ok());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **TIP**: We still can convert the generated [JSON] into a [GraphQL schema language][6] representation by using tools like [`graphql-json-to-sdl` command line utility][30].
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[`EmptyMutation`]: https://docs.rs/juniper/0.16.1/juniper/struct.EmptyMutation.html
|
||||||
|
[`EmptySubscription`]: https://docs.rs/juniper/0.16.1/juniper/struct.EmptySubscription.html
|
||||||
|
[`juniper`]: https://docs.rs/juniper
|
||||||
|
[`juniper-from-schema`]: https://docs.rs/juniper-from-schema
|
||||||
|
[`RootNode`]: https://docs.rs/juniper/0.16.1/juniper/struct.RootNode.html
|
||||||
|
[code-first]: https://www.apollographql.com/blog/backend/architecture/schema-first-vs-code-only-graphql#code-only
|
||||||
|
[schema-first]: https://www.apollographql.com/blog/backend/architecture/schema-first-vs-code-only-graphql#schema-first
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
[JSON]: https://www.json.org
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
[Rust]: https://www.rust-lang.org
|
||||||
|
[schema]: https://graphql.org/learn/schema
|
||||||
|
|
||||||
|
[0]: https://spec.graphql.org/October2021#sec-Schema
|
||||||
|
[1]: https://spec.graphql.org/October2021#sel-FAHTRFCAACChCtpG
|
||||||
|
[2]: https://spec.graphql.org/October2021#sel-FAHTRHCAACCuE9yD
|
||||||
|
[3]: https://spec.graphql.org/October2021#sel-FAHTRJCAACC3EhsX
|
||||||
|
[4]: https://spec.graphql.org/October2021#sec-Objects
|
||||||
|
[5]: https://spec.graphql.org/October2021#sec-Root-Operation-Types.Default-Root-Operation-Type-Names
|
||||||
|
[6]: https://graphql.org/learn/schema#type-language
|
||||||
|
[20]: https://docs.rs/juniper/0.16.1/juniper/struct.RootNode.html#method.as_sdl
|
||||||
|
[21]: https://docs.rs/crate/juniper/latest/source/src/introspection/query.graphql
|
||||||
|
[22]: https://docs.rs/juniper/0.16.1/juniper/fn.introspect.html
|
||||||
|
[30]: https://npmjs.com/package/graphql-json-to-sdl
|
83
book/src/schema/introspection.md
Normal file
83
book/src/schema/introspection.md
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
Introspection
|
||||||
|
=============
|
||||||
|
|
||||||
|
> The [schema introspection][1] system is accessible from the meta-fields `__schema` and `__type` which are accessible from the type of the root of a query operation.
|
||||||
|
> ```graphql
|
||||||
|
> __schema: __Schema!
|
||||||
|
> __type(name: String!): __Type
|
||||||
|
> ```
|
||||||
|
> Like all meta-fields, these are implicit and do not appear in the fields list in the root type of the query operation.
|
||||||
|
|
||||||
|
[GraphQL] provides [introspection][0], allowing to see what [queries][2], [mutations][3] and [subscriptions][4] a [GraphQL] server supports at runtime.
|
||||||
|
|
||||||
|
Because [introspection][0] queries are just regular [GraphQL queries][2], [Juniper] supports them natively. For example, to get all the names of the types supported, we could [execute][5] the following [query][2] against [Juniper]:
|
||||||
|
```graphql
|
||||||
|
{
|
||||||
|
__schema {
|
||||||
|
types {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Disabling
|
||||||
|
|
||||||
|
> Disabling introspection in production is a widely debated topic, but we believe it’s one of the first things you can do to harden your GraphQL API in production.
|
||||||
|
|
||||||
|
[Some security requirements and considerations][10] may mandate to disable [GraphQL schema introspection][1] in production environments. In [Juniper] this can be achieved by using the [`RootNode::disable_introspection()`][9] method:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::{
|
||||||
|
# graphql_object, graphql_vars, EmptyMutation, EmptySubscription, GraphQLError,
|
||||||
|
# RootNode,
|
||||||
|
# };
|
||||||
|
#
|
||||||
|
pub struct Query;
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
impl Query {
|
||||||
|
fn some() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new())
|
||||||
|
.disable_introspection();
|
||||||
|
|
||||||
|
let query = "query { __schema { queryType { name } } }";
|
||||||
|
|
||||||
|
match juniper::execute_sync(query, None, &schema, &graphql_vars! {}, &()) {
|
||||||
|
Err(GraphQLError::ValidationError(errs)) => {
|
||||||
|
assert_eq!(
|
||||||
|
errs.first().unwrap().message(),
|
||||||
|
"GraphQL introspection is not allowed, but the operation contained `__schema`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
res => panic!("expected `ValidationError`, returned: {res:#?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
> **NOTE**: Attempt to execute an [introspection query][1] results in [validation][11] error, rather than [execution][5] error.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
|
||||||
|
[0]: https://spec.graphql.org/October2021#sec-Introspection
|
||||||
|
[1]: https://spec.graphql.org/October2021#sec-Schema-Introspection
|
||||||
|
[2]: https://spec.graphql.org/October2021#sel-GAFRJBABABF_jB
|
||||||
|
[3]: https://spec.graphql.org/October2021#sel-GAFRJDABABI5C
|
||||||
|
[4]: https://spec.graphql.org/October2021#sel-GAFRJFABABMvpN
|
||||||
|
[5]: https://spec.graphql.org/October2021#sec-Execution
|
||||||
|
[9]: https://docs.rs/juniper/0.16.1/juniper/struct.RootNode.html#method.disable_introspection
|
||||||
|
[10]: https://www.apollographql.com/blog/why-you-should-disable-graphql-introspection-in-production
|
||||||
|
[11]: https://spec.graphql.org/October2021#sec-Validation
|
|
@ -1,119 +0,0 @@
|
||||||
# Schemas
|
|
||||||
|
|
||||||
Juniper follows a [code-first approach][schema_approach] to defining GraphQL schemas. If you would like to use a [schema-first approach][schema_approach] instead, consider [juniper-from-schema][] for generating code from a schema file.
|
|
||||||
|
|
||||||
A schema consists of three types: a query object, a mutation object, and a subscription object.
|
|
||||||
These three define the root query fields, mutations and subscriptions of the schema, respectively.
|
|
||||||
|
|
||||||
The usage of subscriptions is a little different from the mutation and query objects, so there is a specific [section][section] that discusses them.
|
|
||||||
|
|
||||||
Both query and mutation objects are regular GraphQL objects, defined like any
|
|
||||||
other object in Juniper. The mutation and subscription objects, however, are optional since schemas
|
|
||||||
can be read-only and do not require subscriptions. If mutation/subscription functionality is not needed, consider using [EmptyMutation][EmptyMutation]/[EmptySubscription][EmptySubscription].
|
|
||||||
|
|
||||||
In Juniper, the `RootNode` type represents a schema. When the schema is first created,
|
|
||||||
Juniper will traverse the entire object graph
|
|
||||||
and register all types it can find. This means that if you define a GraphQL
|
|
||||||
object somewhere but never reference it, it will not be exposed in a schema.
|
|
||||||
|
|
||||||
## The query root
|
|
||||||
|
|
||||||
The query root is just a GraphQL object. You define it like any other GraphQL
|
|
||||||
object in Juniper, most commonly using the `graphql_object` proc macro:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# #![allow(unused_variables)]
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::{graphql_object, FieldResult, GraphQLObject};
|
|
||||||
# #[derive(GraphQLObject)] struct User { name: String }
|
|
||||||
struct Root;
|
|
||||||
|
|
||||||
#[graphql_object]
|
|
||||||
impl Root {
|
|
||||||
fn userWithUsername(username: String) -> FieldResult<Option<User>> {
|
|
||||||
// Look up user in database...
|
|
||||||
# unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() { }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mutations
|
|
||||||
|
|
||||||
Mutations are _also_ just GraphQL objects. Each mutation is a single field
|
|
||||||
that performs some mutating side-effect such as updating a database.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# #![allow(unused_variables)]
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::{graphql_object, FieldResult, GraphQLObject};
|
|
||||||
# #[derive(GraphQLObject)] struct User { name: String }
|
|
||||||
struct Mutations;
|
|
||||||
|
|
||||||
#[graphql_object]
|
|
||||||
impl Mutations {
|
|
||||||
fn signUpUser(name: String, email: String) -> FieldResult<User> {
|
|
||||||
// Validate inputs and save user in database...
|
|
||||||
# unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() { }
|
|
||||||
```
|
|
||||||
|
|
||||||
# Converting a Rust schema to the [GraphQL Schema Language][schema_language]
|
|
||||||
|
|
||||||
Many tools in the GraphQL ecosystem require the schema to be defined in the [GraphQL Schema Language][schema_language]. You can generate a [GraphQL Schema Language][schema_language] representation of your schema defined in Rust using the `schema-language` feature (on by default):
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
use juniper::{
|
|
||||||
graphql_object, EmptyMutation, EmptySubscription, FieldResult, RootNode,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Query;
|
|
||||||
|
|
||||||
#[graphql_object]
|
|
||||||
impl Query {
|
|
||||||
fn hello(&self) -> FieldResult<&str> {
|
|
||||||
Ok("hello world")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
// Define our schema in Rust.
|
|
||||||
let schema = RootNode::new(
|
|
||||||
Query,
|
|
||||||
EmptyMutation::<()>::new(),
|
|
||||||
EmptySubscription::<()>::new(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Convert the Rust schema into the GraphQL Schema Language.
|
|
||||||
let result = schema.as_schema_language();
|
|
||||||
|
|
||||||
let expected = "\
|
|
||||||
type Query {
|
|
||||||
hello: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
schema {
|
|
||||||
query: Query
|
|
||||||
}
|
|
||||||
";
|
|
||||||
# #[cfg(not(target_os = "windows"))]
|
|
||||||
assert_eq!(result, expected);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Note the `schema-language` feature may be turned off if you do not need this functionality to reduce dependencies and speed up
|
|
||||||
compile times.
|
|
||||||
|
|
||||||
|
|
||||||
[schema_language]: https://graphql.org/learn/schema/#type-language
|
|
||||||
[juniper-from-schema]: https://github.com/davidpdrsn/juniper-from-schema
|
|
||||||
[schema_approach]: https://blog.logrocket.com/code-first-vs-schema-first-development-graphql/
|
|
||||||
[section]: ../advanced/subscriptions.md
|
|
||||||
[EmptyMutation]: https://docs.rs/juniper/0.14.2/juniper/struct.EmptyMutation.html
|
|
||||||
<!--TODO: Fix This URL when the EmptySubscription become available in the Documentation -->
|
|
||||||
[EmptySubscription]: https://docs.rs/juniper/0.14.2/juniper/struct.EmptySubscription.html
|
|
176
book/src/schema/subscriptions.md
Normal file
176
book/src/schema/subscriptions.md
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
Subscriptions
|
||||||
|
=============
|
||||||
|
|
||||||
|
[GraphQL subscriptions][9] are a way to push data from a server to clients requesting real-time messages from a server. [Subscriptions][9] are similar to [queries][7] in that they specify a set of fields to be delivered to a client, but instead of immediately returning a single answer a result is sent every time a particular event happens on a server.
|
||||||
|
|
||||||
|
In order to execute [subscriptions][9] in [Juniper], we need a coordinator (spawning long-lived connections) and a [GraphQL object][4] with [fields][5] resolving into a [`Stream`] of elements which will then be returned to a client. The [`juniper_subscriptions` crate][30] provides a default implementation of these abstractions.
|
||||||
|
|
||||||
|
The [subscription root][3] is just a [GraphQL object][4], similar to the [query root][1] and [mutations root][2] that we define for operations in our [GraphQL schema][0]. For [subscriptions][9] all fields should be `async` and return a [`Stream`] of some [GraphQL type][6] values, rather than direct values.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# extern crate futures;
|
||||||
|
# extern crate juniper;
|
||||||
|
# use std::pin::Pin;
|
||||||
|
# use futures::Stream;
|
||||||
|
# use juniper::{graphql_object, graphql_subscription, FieldError};
|
||||||
|
#
|
||||||
|
# #[derive(Clone)]
|
||||||
|
# pub struct Database;
|
||||||
|
#
|
||||||
|
# impl juniper::Context for Database {}
|
||||||
|
#
|
||||||
|
# pub struct Query;
|
||||||
|
#
|
||||||
|
# #[graphql_object]
|
||||||
|
# #[graphql(context = Database)]
|
||||||
|
# impl Query {
|
||||||
|
# fn hello_world() -> &'static str {
|
||||||
|
# "Hello World!"
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
type StringStream = Pin<Box<dyn Stream<Item = Result<String, FieldError>> + Send>>;
|
||||||
|
|
||||||
|
pub struct Subscription;
|
||||||
|
|
||||||
|
#[graphql_subscription]
|
||||||
|
#[graphql(context = Database)]
|
||||||
|
impl Subscription {
|
||||||
|
// This subscription operation emits two values sequentially:
|
||||||
|
// the `String`s "Hello" and "World!".
|
||||||
|
async fn hello_world() -> StringStream {
|
||||||
|
let stream = futures::stream::iter([
|
||||||
|
Ok(String::from("Hello")),
|
||||||
|
Ok(String::from("World!")),
|
||||||
|
]);
|
||||||
|
Box::pin(stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main () {}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Coordinator
|
||||||
|
|
||||||
|
[GraphQL subscriptions][9] require a bit more resources than regular [queries][7] and provide a great vector for [DoS attacks][20]. This can can bring down a server easily if not handled correctly. The [`SubscriptionCoordinator` trait][`SubscriptionCoordinator`] provides coordination logic to enable functionality like [DoS attacks][20] mitigation and resource limits.
|
||||||
|
|
||||||
|
The [`SubscriptionCoordinator`] contains the [schema][0] and can keep track of opened connections, handle [subscription][9] start and end, and maintain a global ID for each [subscription][9]. Each time a connection is established, the [`SubscriptionCoordinator`] spawns a [32], which handles a single connection, providing resolver logic for a client stream as well as reconnection and shutdown logic.
|
||||||
|
|
||||||
|
While we can implement [`SubscriptionCoordinator`] ourselves, [Juniper] contains a simple and generic implementation called [`Coordinator`]. The `subscribe` method returns a [`Future`] resolving into a `Result<Connection, GraphQLError>`, where [`Connection`] is a [`Stream`] of [values][10] returned by the operation, and a [`GraphQLError`] is the error when the [subscription operation][9] fails.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# extern crate futures;
|
||||||
|
# extern crate juniper;
|
||||||
|
# extern crate juniper_subscriptions;
|
||||||
|
# extern crate serde_json;
|
||||||
|
# use std::pin::Pin;
|
||||||
|
# use futures::{Stream, StreamExt as _};
|
||||||
|
# use juniper::{
|
||||||
|
# http::GraphQLRequest,
|
||||||
|
# graphql_object, graphql_subscription,
|
||||||
|
# DefaultScalarValue, EmptyMutation, FieldError,
|
||||||
|
# RootNode, SubscriptionCoordinator,
|
||||||
|
# };
|
||||||
|
# use juniper_subscriptions::Coordinator;
|
||||||
|
#
|
||||||
|
# #[derive(Clone)]
|
||||||
|
# pub struct Database;
|
||||||
|
#
|
||||||
|
# impl juniper::Context for Database {}
|
||||||
|
#
|
||||||
|
# impl Database {
|
||||||
|
# fn new() -> Self {
|
||||||
|
# Self
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# pub struct Query;
|
||||||
|
#
|
||||||
|
# #[graphql_object]
|
||||||
|
# #[graphql(context = Database)]
|
||||||
|
# impl Query {
|
||||||
|
# fn hello_world() -> &'static str {
|
||||||
|
# "Hello World!"
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# type StringStream = Pin<Box<dyn Stream<Item = Result<String, FieldError>> + Send>>;
|
||||||
|
#
|
||||||
|
# pub struct Subscription;
|
||||||
|
#
|
||||||
|
# #[graphql_subscription]
|
||||||
|
# #[graphql(context = Database)]
|
||||||
|
# impl Subscription {
|
||||||
|
# async fn hello_world() -> StringStream {
|
||||||
|
# let stream = futures::stream::iter([
|
||||||
|
# Ok(String::from("Hello")),
|
||||||
|
# Ok(String::from("World!")),
|
||||||
|
# ]);
|
||||||
|
# Box::pin(stream)
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
type Schema = RootNode<'static, Query, EmptyMutation<Database>, Subscription>;
|
||||||
|
|
||||||
|
fn schema() -> Schema {
|
||||||
|
Schema::new(Query, EmptyMutation::new(), Subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_subscription() {
|
||||||
|
let schema = schema();
|
||||||
|
let coordinator = Coordinator::new(schema);
|
||||||
|
let db = Database::new();
|
||||||
|
|
||||||
|
let req: GraphQLRequest<DefaultScalarValue> = serde_json::from_str(
|
||||||
|
r#"{
|
||||||
|
"query": "subscription { helloWorld }"
|
||||||
|
}"#,
|
||||||
|
).unwrap();
|
||||||
|
|
||||||
|
let mut conn = coordinator.subscribe(&req, &db).await.unwrap();
|
||||||
|
while let Some(result) = conn.next().await {
|
||||||
|
println!("{}", serde_json::to_string(&result).unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## WebSocket
|
||||||
|
|
||||||
|
For information about serving [GraphQL subscriptions][9] over [WebSocket], see the ["Serving" chapter](../serve/index.md#websocket).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[`Coordinator`]: https://docs.rs/juniper_subscriptions/0.17.0/juniper_subscriptions/struct.Coordinator.html
|
||||||
|
[`Connection`]: https://docs.rs/juniper_subscriptions/0.17.0/juniper_subscriptions/struct.Connection.html
|
||||||
|
[`Future`]: https://doc.rust-lang.org/stable/std/future/trait.Future.html
|
||||||
|
[`GraphQLError`]: https://docs.rs/juniper/0.16.1/juniper/enum.GraphQLError.html
|
||||||
|
[`Stream`]: https://docs.rs/futures/latest/futures/stream/trait.Stream.html
|
||||||
|
[`SubscriptionCoordinator`]: https://docs.rs/juniper/0.16.1/juniper/trait.SubscriptionCoordinator.html
|
||||||
|
[`SubscriptionConnection`]: https://docs.rs/juniper/0.16.1/juniper/trait.SubscriptionConnection.html
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
[Rust]: https://www.rust-lang.org
|
||||||
|
[WebSocket]: https://en.wikipedia.org/wiki/WebSocket
|
||||||
|
|
||||||
|
[0]: https://spec.graphql.org/October2021#sec-Schema
|
||||||
|
[1]: https://spec.graphql.org/October2021#sel-FAHTRFCAACChCtpG
|
||||||
|
[2]: https://spec.graphql.org/October2021#sel-FAHTRHCAACCuE9yD
|
||||||
|
[3]: https://spec.graphql.org/October2021#sel-FAHTRJCAACC3EhsX
|
||||||
|
[4]: https://spec.graphql.org/October2021#sec-Objects
|
||||||
|
[5]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||||
|
[6]: https://spec.graphql.org/October2021#sec-Types
|
||||||
|
[7]: https://spec.graphql.org/October2021#sec-Query
|
||||||
|
[8]: https://spec.graphql.org/October2021#sec-Mutation
|
||||||
|
[9]: https://spec.graphql.org/October2021#sec-Subscription
|
||||||
|
[10]: https://spec.graphql.org/October2021#sec-Values
|
||||||
|
[20]: https://en.wikipedia.org/wiki/Denial-of-service_attack
|
||||||
|
[30]: https://docs.rs/juniper_subscriptions
|
81
book/src/serve/batching.md
Normal file
81
book/src/serve/batching.md
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
Batching
|
||||||
|
========
|
||||||
|
|
||||||
|
The [GraphQL] standard generally assumes that there will be one server request per each client operation to perform (such as a query or mutation). This is conceptually simple but potentially inefficient.
|
||||||
|
|
||||||
|
Some client libraries (such as [`apollo-link-batch-http`][1]) have the ability to batch operations in a single [HTTP] request to save network round-trips and potentially increase performance. There are [some tradeoffs][3], though, that should be considered before [batching operations][2].
|
||||||
|
|
||||||
|
[Juniper]'s [server integration crates](index.md#officially-supported) support [batching multiple operations][2] in a single [HTTP] request out-of-the-box via [JSON] arrays. This makes them compatible with client libraries that support [batch operations][2] without any special configuration.
|
||||||
|
|
||||||
|
> **NOTE**: If you use a custom server integration, it's **not a hard requirement** to support [batching][2], as it's not a part of the [official GraphQL specification][0].
|
||||||
|
|
||||||
|
Assuming an integration supports [operations batching][2], for the following GraphQL query:
|
||||||
|
```graphql
|
||||||
|
{
|
||||||
|
hero {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The [JSON] `data` to [POST] for an individual request would be:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"query": "{hero{name}}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
And the response would be in the form:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"hero": {
|
||||||
|
"name": "R2-D2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
However, if we want to run the same query twice in a single [HTTP] request, the batched [JSON] `data` to [POST] would be:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"query": "{hero{name}}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"query": "{hero{name}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
And then, the response would be in the following array form:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"hero": {
|
||||||
|
"name": "R2-D2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"hero": {
|
||||||
|
"name": "R2-D2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
[HTTP]: https://en.wikipedia.org/wiki/HTTP
|
||||||
|
[JSON]: https://www.json.org
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
[POST]: https://en.wikipedia.org/wiki/POST_(HTTP)
|
||||||
|
|
||||||
|
[0]: https://spec.graphql.org/October2021
|
||||||
|
[1]: https://www.apollographql.com/docs/link/links/batch-http.html
|
||||||
|
[2]: https://www.apollographql.com/blog/batching-client-graphql-queries
|
||||||
|
[3]: https://www.apollographql.com/blog/batching-client-graphql-queries#what-are-the-tradeoffs-with-batching
|
69
book/src/serve/index.md
Normal file
69
book/src/serve/index.md
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
Serving
|
||||||
|
=======
|
||||||
|
|
||||||
|
Once we have built a [GraphQL schema][1], the next obvious step would be to serve it, so clients can interact with our [GraphQL] API. Usually, [GraphQL] APIs are served via [HTTP].
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Web server frameworks
|
||||||
|
|
||||||
|
Though the [`juniper`] crate doesn't provide a built-in [HTTP] server, the surrounding ecosystem does.
|
||||||
|
|
||||||
|
|
||||||
|
### Officially supported
|
||||||
|
|
||||||
|
[Juniper] officially supports the following widely used and adopted web server frameworks in [Rust] ecosystem:
|
||||||
|
- [`actix-web`] ([`juniper_actix`] crate)
|
||||||
|
- [`axum`] ([`juniper_axum`] crate)
|
||||||
|
- [`hyper`] ([`juniper_hyper`] crate)
|
||||||
|
- [`rocket`] ([`juniper_rocket`] crate)
|
||||||
|
- [`warp`] ([`juniper_warp`] crate)
|
||||||
|
|
||||||
|
See their API docs and usage examples (accessible from API docs) for further details of how they should be used.
|
||||||
|
|
||||||
|
> **NOTE**: All the officially supported web server framework integrations provide a simple and convenient way for exposing [GraphiQL] and/or [GraphQL Playground] with the [GraphQL schema][1] along. These powerful tools ease the development process by enabling you to explore and send client requests to the [GraphQL] API under development.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## WebSocket
|
||||||
|
|
||||||
|
> **NOTE**: [WebSocket] is a crucial part for serving [GraphQL subscriptions][2] over [HTTP].
|
||||||
|
|
||||||
|
There are two widely adopted protocols for serving [GraphQL] over [WebSocket]:
|
||||||
|
1. [Legacy `graphql-ws` GraphQL over WebSocket Protocol][ws-old], formerly used by [Apollo] and the [`subscriptions-transport-ws` npm package], and now being deprecated.
|
||||||
|
2. [New `graphql-transport-ws` GraphQL over WebSocket Protocol][ws-new], provided by the [`graphql-ws` npm package] and being used by [Apollo] as for now.
|
||||||
|
|
||||||
|
In the [Juniper] ecosystem, both implementations are provided by the [`juniper_graphql_ws`] crate. Most of the [officially supported web server framework integrations](#officially-supported) are able to serve a [GraphQL schema][1] over [WebSocket] (including [subscriptions][2]) and even support [auto-negotiation of the correct protocol based on the `Sec-Websocket-Protocol` HTTP header value][3]. See their API docs and usage examples (accessible from API docs) for further details of how to do so.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[`actix-web`]: https://docs.rs/actix-web
|
||||||
|
[`axum`]: https://docs.rs/axum
|
||||||
|
[`graphql-ws` npm package]: https://npmjs.com/package/graphql-ws
|
||||||
|
[`juniper`]: https://docs.rs/juniper
|
||||||
|
[`juniper_actix`]: https://docs.rs/juniper_actix
|
||||||
|
[`juniper_axum`]: https://docs.rs/juniper_axum
|
||||||
|
[`juniper_graphql_ws`]: https://docs.rs/juniper_graphql_ws
|
||||||
|
[`juniper_rocket`]: https://docs.rs/juniper_rocket
|
||||||
|
[`juniper_warp`]: https://docs.rs/juniper_warp
|
||||||
|
[`hyper`]: https://docs.rs/hyper
|
||||||
|
[`rocket`]: https://docs.rs/rocket
|
||||||
|
[`subscriptions-transport-ws` npm package]: https://npmjs.com/package/subscriptions-transport-ws
|
||||||
|
[`warp`]: https://docs.rs/warp
|
||||||
|
[Apollo]: https://www.apollographql.com
|
||||||
|
[GraphiQL]: https://github.com/graphql/graphiql
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
[GraphQL Playground]: https://github.com/prisma/graphql-playground
|
||||||
|
[HTTP]: https://en.wikipedia.org/wiki/HTTP
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
[Rust]: https://www.rust-lang.org
|
||||||
|
[WebSocket]: https://en.wikipedia.org/wiki/WebSocket
|
||||||
|
[ws-new]: https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md
|
||||||
|
[ws-old]: https://github.com/apollographql/subscriptions-transport-ws/blob/v0.11.0/PROTOCOL.md
|
||||||
|
|
||||||
|
[1]: ../schema/index.md
|
||||||
|
[2]: ../schema/subscriptions.md
|
||||||
|
[3]: https://developer.mozilla.org/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#subprotocols
|
|
@ -1,28 +0,0 @@
|
||||||
# Integrating with Hyper
|
|
||||||
|
|
||||||
[Hyper] is a fast HTTP implementation that many other Rust web frameworks
|
|
||||||
leverage. It offers asynchronous I/O via the tokio runtime and works on
|
|
||||||
Rust's stable channel.
|
|
||||||
|
|
||||||
Hyper is not a higher-level web framework and accordingly
|
|
||||||
does not include ergonomic features such as simple endpoint routing,
|
|
||||||
baked-in HTTP responses, or reusable middleware. For GraphQL, those aren't
|
|
||||||
large downsides as all POSTs and GETs usually go through a single endpoint with
|
|
||||||
a few clearly-defined response payloads.
|
|
||||||
|
|
||||||
Juniper's Hyper integration is contained in the [`juniper_hyper`][juniper_hyper] crate:
|
|
||||||
|
|
||||||
!FILENAME Cargo.toml
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
juniper = "0.16.0"
|
|
||||||
juniper_hyper = "0.9.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
Included in the source is a [small example][example] which sets up a basic GraphQL and [GraphiQL] handler.
|
|
||||||
|
|
||||||
[graphiql]: https://github.com/graphql/graphiql
|
|
||||||
[hyper]: https://hyper.rs/
|
|
||||||
[juniper_hyper]: https://github.com/graphql-rust/juniper/tree/master/juniper_hyper
|
|
||||||
[example]: https://github.com/graphql-rust/juniper/blob/master/juniper_hyper/examples/hyper_server.rs
|
|
|
@ -1,17 +0,0 @@
|
||||||
# Adding A Server
|
|
||||||
|
|
||||||
To allow using Juniper with the HTTP server of your choice,
|
|
||||||
it does **not** come with a built in HTTP server.
|
|
||||||
|
|
||||||
To actually get a server up and running, there are multiple official and
|
|
||||||
third-party integration crates that will get you there.
|
|
||||||
|
|
||||||
- [Official Server Integrations](official.md)
|
|
||||||
- [Warp](warp.md)
|
|
||||||
- [Rocket](rocket.md)
|
|
||||||
- [Iron](iron.md)
|
|
||||||
- [Hyper](hyper.md)
|
|
||||||
- [Third Party Integrations](third-party.md)
|
|
||||||
- [Actix-Web](https://github.com/actix/examples/tree/master/juniper)
|
|
||||||
- [Finchers](https://github.com/finchers-rs/finchers-juniper)
|
|
||||||
- [Tsukuyomi](https://github.com/tsukuyomi-rs/tsukuyomi/tree/master/examples/juniper)
|
|
|
@ -1,122 +0,0 @@
|
||||||
# Integrating with Iron
|
|
||||||
|
|
||||||
[Iron] is a library that's been around for a while in the Rust sphere but lately
|
|
||||||
hasn't seen much of development. Nevertheless, it's still a solid library with a
|
|
||||||
familiar request/response/middleware architecture that works on Rust's stable
|
|
||||||
channel.
|
|
||||||
|
|
||||||
Juniper's Iron integration is contained in the `juniper_iron` crate:
|
|
||||||
|
|
||||||
!FILENAME Cargo.toml
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
juniper = "0.16.0"
|
|
||||||
juniper_iron = "0.8.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
Included in the source is a [small
|
|
||||||
example](https://github.com/graphql-rust/juniper_iron/blob/master/examples/iron_server.rs)
|
|
||||||
which sets up a basic GraphQL and [GraphiQL] handler.
|
|
||||||
|
|
||||||
## Basic integration
|
|
||||||
|
|
||||||
Let's start with a minimal schema and just get a GraphQL endpoint up and
|
|
||||||
running. We use [mount] to attach the GraphQL handler at `/graphql`.
|
|
||||||
|
|
||||||
The `context_factory` function will be executed on every request and can be used
|
|
||||||
to set up database connections, read session token information from cookies, and
|
|
||||||
set up other global data that the schema might require.
|
|
||||||
|
|
||||||
In this example, we won't use any global data so we just return an empty value.
|
|
||||||
|
|
||||||
```rust,ignore
|
|
||||||
extern crate juniper;
|
|
||||||
extern crate juniper_iron;
|
|
||||||
extern crate iron;
|
|
||||||
extern crate mount;
|
|
||||||
|
|
||||||
use mount::Mount;
|
|
||||||
use iron::prelude::*;
|
|
||||||
use juniper::EmptyMutation;
|
|
||||||
use juniper_iron::GraphQLHandler;
|
|
||||||
|
|
||||||
fn context_factory(_: &mut Request) -> IronResult<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Root;
|
|
||||||
|
|
||||||
#[juniper::graphql_object]
|
|
||||||
impl Root {
|
|
||||||
fn foo() -> String {
|
|
||||||
"Bar".into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# #[allow(unreachable_code, unused_variables)]
|
|
||||||
fn main() {
|
|
||||||
let mut mount = Mount::new();
|
|
||||||
|
|
||||||
let graphql_endpoint = GraphQLHandler::new(
|
|
||||||
context_factory,
|
|
||||||
Root,
|
|
||||||
EmptyMutation::<()>::new(),
|
|
||||||
);
|
|
||||||
|
|
||||||
mount.mount("/graphql", graphql_endpoint);
|
|
||||||
|
|
||||||
let chain = Chain::new(mount);
|
|
||||||
|
|
||||||
# return;
|
|
||||||
Iron::new(chain).http("0.0.0.0:8080").unwrap();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Accessing data from the request
|
|
||||||
|
|
||||||
If you want to access e.g. the source IP address of the request from a field
|
|
||||||
resolver, you need to pass this data using Juniper's [context feature](../types/objects/using_contexts.md).
|
|
||||||
|
|
||||||
```rust,ignore
|
|
||||||
# extern crate juniper;
|
|
||||||
# extern crate juniper_iron;
|
|
||||||
# extern crate iron;
|
|
||||||
# use iron::prelude::*;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
|
|
||||||
struct Context {
|
|
||||||
remote_addr: SocketAddr,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl juniper::Context for Context {}
|
|
||||||
|
|
||||||
fn context_factory(req: &mut Request) -> IronResult<Context> {
|
|
||||||
Ok(Context {
|
|
||||||
remote_addr: req.remote_addr
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Root;
|
|
||||||
|
|
||||||
#[juniper::graphql_object(
|
|
||||||
Context = Context,
|
|
||||||
)]
|
|
||||||
impl Root {
|
|
||||||
field my_addr(context: &Context) -> String {
|
|
||||||
format!("Hello, you're coming from {}", context.remote_addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# fn main() {
|
|
||||||
# let _graphql_endpoint = juniper_iron::GraphQLHandler::new(
|
|
||||||
# context_factory,
|
|
||||||
# Root,
|
|
||||||
# juniper::EmptyMutation::<Context>::new(),
|
|
||||||
# );
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
|
|
||||||
[iron]: https://github.com/iron/iron
|
|
||||||
[graphiql]: https://github.com/graphql/graphiql
|
|
||||||
[mount]: https://github.com/iron/mount
|
|
|
@ -1,9 +0,0 @@
|
||||||
# Official Server Integrations
|
|
||||||
|
|
||||||
Juniper provides official integration crates for several popular Rust server
|
|
||||||
libraries.
|
|
||||||
|
|
||||||
- [Warp](warp.md)
|
|
||||||
- [Rocket](rocket.md)
|
|
||||||
- [Iron](iron.md)
|
|
||||||
- [Hyper](hyper.md)
|
|
|
@ -1,22 +0,0 @@
|
||||||
# Integrating with Rocket
|
|
||||||
|
|
||||||
[Rocket] is a web framework for Rust that makes it simple to write fast web applications without sacrificing flexibility or type safety. All with minimal code. Rocket
|
|
||||||
does not work on Rust's stable channel and instead requires the nightly
|
|
||||||
channel.
|
|
||||||
|
|
||||||
Juniper's Rocket integration is contained in the [`juniper_rocket`][juniper_rocket] crate:
|
|
||||||
|
|
||||||
!FILENAME Cargo.toml
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
juniper = "0.16.0"
|
|
||||||
juniper_rocket = "0.9.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
Included in the source is a [small example][example] which sets up a basic GraphQL and [GraphiQL] handler.
|
|
||||||
|
|
||||||
[graphiql]: https://github.com/graphql/graphiql
|
|
||||||
[rocket]: https://rocket.rs/
|
|
||||||
[juniper_rocket]: https://github.com/graphql-rust/juniper/tree/master/juniper_rocket
|
|
||||||
[example]: https://github.com/graphql-rust/juniper/blob/master/juniper_rocket/examples/rocket_server.rs
|
|
|
@ -1,5 +0,0 @@
|
||||||
# Other Examples
|
|
||||||
|
|
||||||
These examples are not officially maintained by Juniper developers.
|
|
||||||
|
|
||||||
- [Actix Web](https://github.com/actix/examples/tree/HEAD/graphql/juniper) | [Actix Web (advanced)](https://github.com/actix/examples/tree/HEAD/graphql/juniper-advanced)
|
|
|
@ -1,23 +0,0 @@
|
||||||
# Integrating with Warp
|
|
||||||
|
|
||||||
[Warp] is a super-easy, composable, web server framework for warp speeds.
|
|
||||||
The fundamental building block of warp is the Filter: they can be combined and composed to express rich requirements on requests. Warp is built on [Hyper] and works on
|
|
||||||
Rust's stable channel.
|
|
||||||
|
|
||||||
Juniper's Warp integration is contained in the [`juniper_warp`][juniper_warp] crate:
|
|
||||||
|
|
||||||
!FILENAME Cargo.toml
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
juniper = "0.16.0"
|
|
||||||
juniper_warp = "0.8.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
Included in the source is a [small example][example] which sets up a basic GraphQL and [GraphiQL] handler.
|
|
||||||
|
|
||||||
[graphiql]: https://github.com/graphql/graphiql
|
|
||||||
[hyper]: https://hyper.rs/
|
|
||||||
[warp]: https://crates.io/crates/warp
|
|
||||||
[juniper_warp]: https://github.com/graphql-rust/juniper/tree/master/juniper_warp
|
|
||||||
[example]: https://github.com/graphql-rust/juniper/blob/master/juniper_warp/examples/warp_server.rs
|
|
|
@ -1,73 +1,151 @@
|
||||||
# Enums
|
Enums
|
||||||
|
=====
|
||||||
|
|
||||||
Enums in GraphQL are string constants grouped together to represent a set of
|
> [GraphQL enum][0] types, like [scalar][1] types, also represent leaf values in a GraphQL type system. However [enum][0] types describe the set of possible values.
|
||||||
possible values. Simple Rust enums can be converted to GraphQL enums by using a
|
>
|
||||||
custom derive attribute:
|
> [Enums][0] are not references for a numeric value, but are unique values in their own right. They may serialize as a string: the name of the represented value.
|
||||||
|
|
||||||
|
With [Juniper] a [GraphQL enum][0] may be defined by using the [`#[derive(GraphQLEnum)]`][2] attribute on a [Rust enum][3] as long as its variants do not have any fields:
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
#[derive(juniper::GraphQLEnum)]
|
# use juniper::GraphQLEnum;
|
||||||
|
#
|
||||||
|
#[derive(GraphQLEnum)]
|
||||||
enum Episode {
|
enum Episode {
|
||||||
NewHope,
|
NewHope,
|
||||||
Empire,
|
Empire,
|
||||||
Jedi,
|
Jedi,
|
||||||
}
|
}
|
||||||
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
Juniper converts all enum variants to uppercase, so the corresponding string
|
|
||||||
values for these variants are `NEWHOPE`, `EMPIRE`, and `JEDI`, respectively. If
|
|
||||||
you want to override this, you can use the `graphql` attribute, similar to how
|
|
||||||
it works when [defining objects](objects/defining_objects.md):
|
|
||||||
|
|
||||||
|
### Renaming
|
||||||
|
|
||||||
|
By default, [enum][3] variants are converted from [Rust]'s standard `PascalCase` naming convention into [GraphQL]'s `SCREAMING_SNAKE_CASE` convention:
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
#[derive(juniper::GraphQLEnum)]
|
# use juniper::GraphQLEnum;
|
||||||
|
#
|
||||||
|
#[derive(GraphQLEnum)]
|
||||||
enum Episode {
|
enum Episode {
|
||||||
#[graphql(name="NEW_HOPE")]
|
NewHope, // exposed as `NEW_HOPE` in GraphQL schema
|
||||||
NewHope,
|
Empire, // exposed as `EMPIRE` in GraphQL schema
|
||||||
|
Jedi, // exposed as `JEDI` in GraphQL schema
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
We can override the name by using the `#[graphql(name = "...")]` attribute:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::GraphQLEnum;
|
||||||
|
#
|
||||||
|
#[derive(GraphQLEnum)]
|
||||||
|
#[graphql(name = "WrongEpisode")] // now exposed as `WrongEpisode` in GraphQL schema
|
||||||
|
enum Episode {
|
||||||
|
#[graphql(name = "LAST_HOPE")]
|
||||||
|
NewHope, // exposed as `LAST_HOPE` in GraphQL schema
|
||||||
Empire,
|
Empire,
|
||||||
Jedi,
|
Jedi,
|
||||||
}
|
}
|
||||||
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation and deprecation
|
Or provide a different renaming policy for all the [enum][3] variants:
|
||||||
|
|
||||||
Just like when defining objects, the type itself can be renamed and documented,
|
|
||||||
while individual enum variants can be renamed, documented, and deprecated:
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
#[derive(juniper::GraphQLEnum)]
|
# use juniper::GraphQLEnum;
|
||||||
#[graphql(name="Episode", description="An episode of Star Wars")]
|
#
|
||||||
|
#[derive(GraphQLEnum)]
|
||||||
|
#[graphql(rename_all = "none")] // disables any renaming
|
||||||
|
enum Episode {
|
||||||
|
NewHope, // exposed as `NewHope` in GraphQL schema
|
||||||
|
Empire, // exposed as `Empire` in GraphQL schema
|
||||||
|
Jedi, // exposed as `Jedi` in GraphQL schema
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming).
|
||||||
|
|
||||||
|
|
||||||
|
### Documentation and deprecation
|
||||||
|
|
||||||
|
Just like when [defining GraphQL objects](objects/index.md#documentation), the [GraphQL enum][0] type and its values could be [documented][4] and [deprecated][5] via `#[graphql(description = "...")]` and `#[graphql(deprecated = "...")]`/[`#[deprecated]`][13] attributes:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::GraphQLEnum;
|
||||||
|
#
|
||||||
|
/// This doc comment is visible only in Rust API docs.
|
||||||
|
#[derive(GraphQLEnum)]
|
||||||
|
#[graphql(description = "An episode of Star Wars")]
|
||||||
enum StarWarsEpisode {
|
enum StarWarsEpisode {
|
||||||
#[graphql(deprecated="We don't really talk about this one")]
|
/// This doc comment is visible only in Rust API docs.
|
||||||
ThePhantomMenace,
|
#[graphql(description = "This description is visible only in GraphQL schema.")]
|
||||||
|
|
||||||
#[graphql(name="NEW_HOPE")]
|
|
||||||
NewHope,
|
NewHope,
|
||||||
|
|
||||||
#[graphql(description="Arguably the best one in the trilogy")]
|
/// This doc comment is visible only in Rust API docs.
|
||||||
|
#[graphql(desc = "Arguably the best one in the trilogy.")]
|
||||||
|
// ^^^^ shortcut for a `description` argument
|
||||||
|
Empire,
|
||||||
|
|
||||||
|
/// This doc comment is visible in both Rust API docs and GraphQL schema
|
||||||
|
/// descriptions.
|
||||||
|
Jedi,
|
||||||
|
|
||||||
|
#[deprecated(note = "Only visible in Rust.")]
|
||||||
|
#[graphql(deprecated = "We don't really talk about this one.")]
|
||||||
|
// ^^^^^^^^^^ takes precedence over Rust's `#[deprecated]` attribute
|
||||||
|
ThePhantomMenace, // has no description in GraphQL schema
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
> **NOTE**: Only [GraphQL object][6]/[interface][7] fields and [GraphQL enum][0] values can be [deprecated][5].
|
||||||
|
|
||||||
|
|
||||||
|
### Ignoring
|
||||||
|
|
||||||
|
By default, all [enum][3] variants are included in the generated [GraphQL enum][0] type as values. To prevent including a specific variant, annotate it with the `#[graphql(ignore)]` attribute:
|
||||||
|
```rust
|
||||||
|
# #![allow(dead_code)]
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::GraphQLEnum;
|
||||||
|
#
|
||||||
|
#[derive(GraphQLEnum)]
|
||||||
|
enum Episode<T> {
|
||||||
|
NewHope,
|
||||||
Empire,
|
Empire,
|
||||||
Jedi,
|
Jedi,
|
||||||
|
#[graphql(ignore)]
|
||||||
|
Legends(T), // cannot be queried from GraphQL
|
||||||
|
#[graphql(skip)]
|
||||||
|
// ^^^^ alternative naming, up to your preference
|
||||||
|
CloneWars(T), // cannot be queried from GraphQL
|
||||||
}
|
}
|
||||||
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Supported Macro Attributes (Derive)
|
> **TIP**: See more available features in the API docs of the [`#[derive(GraphQLEnum)]`][2] attribute.
|
||||||
|
|
||||||
| Name of Attribute | Container Support | Field Support |
|
|
||||||
|-------------------|:-----------------:|:----------------:|
|
|
||||||
| context | ✔ | ? |
|
|
||||||
| deprecated | ✔ | ✔ |
|
[GraphQL]: https://graphql.org
|
||||||
| description | ✔ | ✔ |
|
[Juniper]: https://docs.rs/juniper
|
||||||
| interfaces | ? | ✘ |
|
[Rust]: https://www.rust-lang.org
|
||||||
| name | ✔ | ✔ |
|
|
||||||
| noasync | ✔ | ? |
|
[0]: https://spec.graphql.org/October2021#sec-Enums
|
||||||
| scalar | ✘ | ? |
|
[1]: https://spec.graphql.org/October2021#sec-Scalars
|
||||||
| skip | ? | ✘ |
|
[2]: https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLEnum.html
|
||||||
| ✔: supported | ✘: not supported | ?: not available |
|
[3]: https://doc.rust-lang.org/reference/items/enumerations.html
|
||||||
|
[4]: https://spec.graphql.org/October2021#sec-Descriptions
|
||||||
|
[5]: https://spec.graphql.org/October2021#sec--deprecated
|
||||||
|
[6]: https://spec.graphql.org/October2021#sec-Objects
|
||||||
|
[7]: https://spec.graphql.org/October2021#sec-Interfaces
|
||||||
|
[13]: https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-deprecated-attribute
|
|
@ -1,20 +1,28 @@
|
||||||
# Type System
|
Type system
|
||||||
|
===========
|
||||||
|
|
||||||
Most of the work in working with juniper consists of mapping the
|
Most of the work in working with [Juniper] consists of mapping the [GraphQL type system][0] to the [Rust] types our application uses.
|
||||||
GraphQL type system to the Rust types your application uses.
|
|
||||||
|
|
||||||
Juniper provides some convenient abstractions that try to make this process
|
[Juniper] provides some convenient abstractions making this process as painless as possible.
|
||||||
as painless as possible.
|
|
||||||
|
|
||||||
Find out more in the individual chapters below.
|
Find out more in the individual chapters below:
|
||||||
|
- [Objects](objects/index.md)
|
||||||
|
- [Complex fields](objects/complex_fields.md)
|
||||||
|
- [Context](objects/Context.md)
|
||||||
|
- [Error handling](objects/error/index.md)
|
||||||
|
- [Field errors](objects/error/field.md)
|
||||||
|
- [Schema errors](objects/error/schema.md)
|
||||||
|
- [Generics](objects/generics.md)
|
||||||
|
- [Interfaces](interfaces.md)
|
||||||
|
- [Unions](unions.md)
|
||||||
|
- [Enums](enums.md)
|
||||||
|
- [Input objects](input_objects.md)
|
||||||
|
- [Scalars](scalars.md)
|
||||||
|
|
||||||
- [Defining objects](objects/defining_objects.md)
|
|
||||||
- [Complex fields](objects/complex_fields.md)
|
|
||||||
- [Using contexts](objects/using_contexts.md)
|
|
||||||
- [Error handling](objects/error_handling.md)
|
[Juniper]: https://docs.rs/juniper
|
||||||
- [Other types](other-index.md)
|
[Rust]: https://www.rust-lang.org
|
||||||
- [Enums](enums.md)
|
|
||||||
- [Interfaces](interfaces.md)
|
[0]: https://spec.graphql.org/October2021#sec-Type-System
|
||||||
- [Input objects](input_objects.md)
|
|
||||||
- [Scalars](scalars.md)
|
|
||||||
- [Unions](unions.md)
|
|
||||||
|
|
|
@ -1,62 +1,163 @@
|
||||||
# Input objects
|
Input objects
|
||||||
|
=============
|
||||||
|
|
||||||
Input objects are complex data structures that can be used as arguments to
|
> [Fields][4] may accept [arguments][5] to configure their behavior. These inputs are often [scalars][12] or [enums][10], but they sometimes need to represent more complex values.
|
||||||
GraphQL fields. In Juniper, you can define input objects using a custom derive
|
>
|
||||||
attribute, similar to simple objects and enums:
|
> A [GraphQL input object][0] defines a set of input fields; the input fields are either [scalars][12], [enums][10], or other [input objects][0]. This allows [arguments][5] to accept arbitrarily complex structs.
|
||||||
|
|
||||||
|
In [Juniper], defining a [GraphQL input object][0] is quite straightforward and similar to how [trivial GraphQL objects are defined](objects/index.md) - by using the [`#[derive(GraphQLInputObject)]` attribute][2] on a [Rust struct][struct]:
|
||||||
```rust
|
```rust
|
||||||
# #![allow(unused_variables)]
|
# #![allow(unused_variables)]
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
#[derive(juniper::GraphQLInputObject)]
|
# use juniper::{graphql_object, GraphQLInputObject, GraphQLObject};
|
||||||
|
#
|
||||||
|
#[derive(GraphQLInputObject)]
|
||||||
struct Coordinate {
|
struct Coordinate {
|
||||||
latitude: f64,
|
latitude: f64,
|
||||||
longitude: f64
|
longitude: f64
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Root;
|
struct Root;
|
||||||
# #[derive(juniper::GraphQLObject)] struct User { name: String }
|
# #[derive(GraphQLObject)] struct User { name: String }
|
||||||
|
|
||||||
#[juniper::graphql_object]
|
#[graphql_object]
|
||||||
impl Root {
|
impl Root {
|
||||||
fn users_at_location(coordinate: Coordinate, radius: f64) -> Vec<User> {
|
fn users_at_location(coordinate: Coordinate, radius: f64) -> Vec<User> {
|
||||||
// Send coordinate to database
|
// Send coordinate to database
|
||||||
// ...
|
// ...
|
||||||
# unimplemented!()
|
# unimplemented!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation and renaming
|
|
||||||
|
|
||||||
Just like the [other](objects/defining_objects.md) [derives](enums.md), you can rename
|
### Renaming
|
||||||
and add documentation to both the type and the fields:
|
|
||||||
|
|
||||||
|
Just as with [defining GraphQL objects](objects/index.md#renaming), by default [struct] fields are converted from [Rust]'s standard `snake_case` naming convention into [GraphQL]'s `camelCase` convention:
|
||||||
```rust
|
```rust
|
||||||
# #![allow(unused_variables)]
|
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
#[derive(juniper::GraphQLInputObject)]
|
# use juniper::GraphQLInputObject;
|
||||||
#[graphql(name="Coordinate", description="A position on the globe")]
|
#
|
||||||
struct WorldCoordinate {
|
#[derive(GraphQLInputObject)]
|
||||||
#[graphql(name="lat", description="The latitude")]
|
struct Person {
|
||||||
latitude: f64,
|
first_name: String, // exposed as `firstName` in GraphQL schema
|
||||||
|
last_name: String, // exposed as `lastName` in GraphQL schema
|
||||||
#[graphql(name="long", description="The longitude")]
|
|
||||||
longitude: f64
|
|
||||||
}
|
}
|
||||||
|
#
|
||||||
struct Root;
|
|
||||||
# #[derive(juniper::GraphQLObject)] struct User { name: String }
|
|
||||||
|
|
||||||
#[juniper::graphql_object]
|
|
||||||
impl Root {
|
|
||||||
fn users_at_location(coordinate: WorldCoordinate, radius: f64) -> Vec<User> {
|
|
||||||
// Send coordinate to database
|
|
||||||
// ...
|
|
||||||
# unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
We can override the name by using the `#[graphql(name = "...")]` attribute:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::GraphQLInputObject;
|
||||||
|
#
|
||||||
|
#[derive(GraphQLInputObject)]
|
||||||
|
#[graphql(name = "WebPerson")] // now exposed as `WebPerson` in GraphQL schema
|
||||||
|
struct Person {
|
||||||
|
name: String,
|
||||||
|
age: i32,
|
||||||
|
#[graphql(name = "websiteURL")]
|
||||||
|
website_url: Option<String>, // now exposed as `websiteURL` in GraphQL schema
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or provide a different renaming policy for all the [struct] fields:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::GraphQLInputObject;
|
||||||
|
#
|
||||||
|
#[derive(GraphQLInputObject)]
|
||||||
|
#[graphql(rename_all = "none")] // disables any renaming
|
||||||
|
struct Person {
|
||||||
|
name: String,
|
||||||
|
age: i32,
|
||||||
|
website_url: Option<String>, // exposed as `website_url` in GraphQL schema
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming).
|
||||||
|
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
Similarly, [GraphQL descriptions][7] may be provided by either using [Rust doc comments][6] or with the `#[graphql(description = "...")]` attribute:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::GraphQLInputObject;
|
||||||
|
#
|
||||||
|
/// This doc comment is visible only in Rust API docs.
|
||||||
|
#[derive(GraphQLInputObject)]
|
||||||
|
#[graphql(description = "This description is visible only in GraphQL schema.")]
|
||||||
|
struct Person {
|
||||||
|
/// This doc comment is visible only in Rust API docs.
|
||||||
|
#[graphql(desc = "This description is visible only in GraphQL schema.")]
|
||||||
|
// ^^^^ shortcut for a `description` argument
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
/// This doc comment is visible in both Rust API docs and GraphQL schema
|
||||||
|
/// descriptions.
|
||||||
|
age: i32,
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
> **NOTE**: As of [October 2021 GraphQL specification][spec], [GraphQL input object][0]'s fields **cannot be** [deprecated][9].
|
||||||
|
|
||||||
|
|
||||||
|
### Ignoring
|
||||||
|
|
||||||
|
By default, all [struct] fields are included into the generated [GraphQL input object][0] type. To prevent inclusion of a specific field annotate it with the `#[graphql(ignore)]` attribute:
|
||||||
|
> **WARNING**: Ignored fields must either implement `Default` or be annotated with the `#[graphql(default = <expression>)]` argument.
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::GraphQLInputObject;
|
||||||
|
#
|
||||||
|
enum System {
|
||||||
|
Cartesian,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(GraphQLInputObject)]
|
||||||
|
struct Point2D {
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
#[graphql(ignore, default = System::Cartesian)]
|
||||||
|
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
// This attribute is required, as we need to be able to construct
|
||||||
|
// a `Point2D` value from the `{ x: 0.0, y: 0.0 }` GraphQL input value,
|
||||||
|
// received from client-side.
|
||||||
|
system: System,
|
||||||
|
// `Default::default()` value is used, if no
|
||||||
|
// `#[graphql(default = <expression>)]` is specified.
|
||||||
|
#[graphql(skip)]
|
||||||
|
// ^^^^ alternative naming, up to your preference
|
||||||
|
shift: f64,
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **TIP**: See more available features in the API docs of the [`#[derive(GraphQLInputObject)]`][2] attribute.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
[Rust]: https://www.rust-lang.org
|
||||||
|
[struct]: https://doc.rust-lang.org/reference/items/structs.html
|
||||||
|
[spec]: https://spec.graphql.org/October2021
|
||||||
|
|
||||||
|
[0]: https://spec.graphql.org/October2021#sec-Input-Objects
|
||||||
|
[2]: https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLInputObject.html
|
||||||
|
[4]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||||
|
[5]: https://spec.graphql.org/October2021#sec-Language.Arguments
|
||||||
|
[6]: https://doc.rust-lang.org/reference/comments.html#doc-comments
|
||||||
|
[7]: https://spec.graphql.org/October2021#sec-Descriptions
|
||||||
|
[9]: https://spec.graphql.org/October2021#sec--deprecated
|
||||||
|
[10]: https://spec.graphql.org/October2021#sec-Enums
|
||||||
|
[12]: https://spec.graphql.org/October2021#sec-Scalars
|
||||||
|
|
|
@ -1,76 +1,45 @@
|
||||||
Interfaces
|
Interfaces
|
||||||
==========
|
==========
|
||||||
|
|
||||||
[GraphQL interfaces][1] map well to interfaces known from common object-oriented languages such as Java or C#, but Rust, unfortunately, has no concept that maps perfectly to them. The nearest analogue of [GraphQL interfaces][1] are Rust traits, and the main difference is that in GraphQL an [interface type][1] serves both as an _abstraction_ and a _boxed value (downcastable to concrete implementers)_, while in Rust, a trait is an _abstraction only_ and _to represent such a boxed value a separate type is required_, like enum or trait object, because Rust trait doesn't represent a type itself, and so can have no values. This difference imposes some unintuitive and non-obvious corner cases when we try to express [GraphQL interfaces][1] in Rust, but on the other hand gives you full control over which type is backing your interface, and how it's resolved.
|
> [GraphQL interfaces][0] represent a list of named [fields][4] and their [arguments][5]. [GraphQL objects][10] and [interfaces][0] can then implement these [interfaces][0] which requires that the implementing type will define all [fields][4] defined by those [interfaces][0].
|
||||||
|
|
||||||
For implementing [GraphQL interfaces][1] Juniper provides the `#[graphql_interface]` macro.
|
[GraphQL interfaces][0] map well to interfaces known from common object-oriented languages such as Java or C#, but [Rust], unfortunately, has no concept that maps perfectly to them. The nearest analogue of [GraphQL interfaces][0] are [Rust traits][20], but the main difference is that in [GraphQL] an [interface type][0] serves both as an _abstraction_ and a _boxed value (dispatchable to concrete implementers)_, while in [Rust], a [trait][20] is an _abstraction only_, and _to represent such a boxed value a separate type is required_, like a [trait object][21] or an [enum][22] consisting of implementer types, because [Rust trait][20] doesn't represent a type itself, and so, can have no values.
|
||||||
|
|
||||||
|
Another notable difference is that [GraphQL interfaces][0] are more like [structurally-typed][30] contracts: they _only declare a list of [fields][4]_ a [GraphQL] type should already have. [Rust traits][20], on the other hand, are [type classes][31], which don't really care about existing methods, but, rather, _require to provide implementations for required methods_ despite the fact whether the type already has such methods or not. This difference makes the [trait implementation][23] not a good fit for expressing a [GraphQL interface][0] implementation, because _we don't really need to implement any [fields][4]_, the [GraphQL] type implementing a [GraphQL interface][0] has those [fields][4] already. _We only need to check that [fields'][4] signatures match_.
|
||||||
|
|
||||||
|
That's why [Juniper] takes the following approach to represent [GraphQL interfaces][0], which consists of two parts:
|
||||||
|
1. Either a [struct][24], or a [trait][20] (in case [fields][4] have [arguments][5]), which acts only as a blueprint describing the required list of [fields][4], and is not used in runtime at all.
|
||||||
|
2. An auto-generated [enum][22], representing a dispatchable value-type for the [GraphQL interfaces][0], which may be referred and returned by other [fields][4].
|
||||||
|
|
||||||
|
This may be done by using either the [`#[graphql_interface]` attribute][3] or the [`#[derive(GraphQLInterface)]`][2]:
|
||||||
## Traits
|
|
||||||
|
|
||||||
Defining a trait is mandatory for defining a [GraphQL interface][1], because this is the _obvious_ way we describe an _abstraction_ in Rust. All [interface][1] fields are defined as computed ones via trait methods.
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
use juniper::graphql_interface;
|
# use juniper::{graphql_interface, GraphQLInterface, GraphQLObject};
|
||||||
|
#
|
||||||
|
// By default a `CharacterValue` enum is generated by macro to represent
|
||||||
|
// values of this GraphQL interface.
|
||||||
|
#[derive(GraphQLInterface)]
|
||||||
|
#[graphql(for = Human)] // enumerating all implementers is mandatory
|
||||||
|
struct Character {
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using a trait to describe the required fields is fine too.
|
||||||
#[graphql_interface]
|
#[graphql_interface]
|
||||||
trait Character {
|
#[graphql(enum = HasHomeEnum, for = Human)]
|
||||||
fn id(&self) -> &str;
|
// ^^^^ the generated value-type enum can be renamed, if required
|
||||||
}
|
trait HasHome {
|
||||||
#
|
fn home_planet(&self) -> &str;
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
However, to return values of such [interface][1], we should provide its implementers and the Rust type representing a _boxed value of this trait_. The last one can be represented in two flavors: enum and [trait object][2].
|
|
||||||
|
|
||||||
|
|
||||||
### Enum values (default)
|
|
||||||
|
|
||||||
By default, Juniper generates an enum representing the values of the defined [GraphQL interface][1], and names it straightforwardly, `{Interface}Value`.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
use juniper::{graphql_interface, GraphQLObject};
|
|
||||||
|
|
||||||
#[graphql_interface(for = [Human, Droid])] // enumerating all implementers is mandatory
|
|
||||||
trait Character {
|
|
||||||
fn id(&self) -> &str;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
#[derive(GraphQLObject)]
|
||||||
#[graphql(impl = CharacterValue)] // notice enum name, NOT trait name
|
#[graphql(impl = [CharacterValue, HasHomeEnum])]
|
||||||
|
// ^^^^^^^^^^^^^^ ^^^^^^^^^^^
|
||||||
|
// Notice the enum type names, neither the trait name nor the struct name
|
||||||
|
// is used to refer the GraphQL interface.
|
||||||
struct Human {
|
struct Human {
|
||||||
id: String,
|
id: String, // also resolves `Character.id` field
|
||||||
}
|
home_planet: String, // also resolves `HasHome.homePlanet` field
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(impl = CharacterValue)]
|
|
||||||
struct Droid {
|
|
||||||
id: String,
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
Also, enum name can be specified explicitly, if desired.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
use juniper::{graphql_interface, GraphQLObject};
|
|
||||||
|
|
||||||
#[graphql_interface(enum = CharaterInterface, for = Human)]
|
|
||||||
trait Character {
|
|
||||||
fn id(&self) -> &str;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(impl = CharaterInterface)]
|
|
||||||
struct Human {
|
|
||||||
id: String,
|
|
||||||
home_planet: String,
|
|
||||||
}
|
}
|
||||||
#
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
|
@ -79,18 +48,19 @@ struct Human {
|
||||||
|
|
||||||
### Interfaces implementing other interfaces
|
### Interfaces implementing other interfaces
|
||||||
|
|
||||||
GraphQL allows implementing interfaces on other interfaces in addition to objects.
|
[GraphQL] allows implementing [interfaces][0] on other [interfaces][0] in addition to [objects][10]:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
use juniper::{graphql_interface, graphql_object, ID};
|
# use juniper::{graphql_object, GraphQLInterface, ID};
|
||||||
|
#
|
||||||
#[graphql_interface(for = [HumanValue, Luke])]
|
#[derive(GraphQLInterface)]
|
||||||
|
#[graphql(for = [HumanValue, Luke])]
|
||||||
struct Node {
|
struct Node {
|
||||||
id: ID,
|
id: ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[graphql_interface(impl = NodeValue, for = Luke)]
|
#[derive(GraphQLInterface)]
|
||||||
|
#[graphql(impl = NodeValue, for = Luke)]
|
||||||
struct Human {
|
struct Human {
|
||||||
id: ID,
|
id: ID,
|
||||||
home_planet: String,
|
home_planet: String,
|
||||||
|
@ -100,15 +70,16 @@ struct Luke {
|
||||||
id: ID,
|
id: ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[graphql_object(impl = [HumanValue, NodeValue])]
|
#[graphql_object]
|
||||||
|
#[graphql(impl = [HumanValue, NodeValue])]
|
||||||
impl Luke {
|
impl Luke {
|
||||||
fn id(&self) -> &ID {
|
fn id(&self) -> &ID {
|
||||||
&self.id
|
&self.id
|
||||||
}
|
}
|
||||||
|
|
||||||
// As `String` and `&str` aren't distinguished by
|
// As `String` and `&str` aren't distinguished by GraphQL spec and
|
||||||
// GraphQL spec, you can use them interchangeably.
|
// represent the same `!String` GraphQL scalar, we can use them
|
||||||
// Same is applied for `Cow<'a, str>`.
|
// interchangeably. The same is applied for `Cow<'a, str>`.
|
||||||
// ⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄
|
// ⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄⌄
|
||||||
fn home_planet() -> &'static str {
|
fn home_planet() -> &'static str {
|
||||||
"Tatooine"
|
"Tatooine"
|
||||||
|
@ -118,78 +89,84 @@ impl Luke {
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
> __NOTE:__ Every interface has to specify all other interfaces/objects it implements or implemented for. Missing one of `for = ` or `impl = ` attributes is a compile-time error.
|
> **NOTE**: Every [interface][0] has to specify all other [interfaces][0]/[objects][0] it implements or implemented for. Missing one of `for = ` or `impl = ` attribute arguments is a **compile-time error**.
|
||||||
|
> ```rust,compile_fail
|
||||||
```compile_fail
|
> # extern crate juniper;
|
||||||
# extern crate juniper;
|
> # use juniper::{GraphQLInterface, GraphQLObject};
|
||||||
use juniper::{graphql_interface, GraphQLObject};
|
> #
|
||||||
|
> #[derive(GraphQLObject)]
|
||||||
#[derive(GraphQLObject)]
|
> pub struct ObjA {
|
||||||
pub struct ObjA {
|
> id: String,
|
||||||
id: String,
|
> }
|
||||||
}
|
>
|
||||||
|
> #[derive(GraphQLInterface)]
|
||||||
#[graphql_interface(for = ObjA)]
|
> #[graphql(for = ObjA)]
|
||||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the evaluated program panicked at
|
> // ^^^^^^^^^^ the evaluated program panicked at
|
||||||
// 'Failed to implement interface `Character` on `ObjA`: missing interface reference in implementer's `impl` attribute.'
|
> // 'Failed to implement interface `Character` on `ObjA`: missing interface
|
||||||
struct Character {
|
> // reference in implementer's `impl` attribute.'
|
||||||
id: String,
|
> struct Character {
|
||||||
}
|
> id: String,
|
||||||
|
> }
|
||||||
fn main() {}
|
> #
|
||||||
```
|
> # fn main() {}
|
||||||
|
> ```
|
||||||
|
|
||||||
|
|
||||||
### GraphQL subtyping and additional `null`able fields
|
### Subtyping and additional `null`able arguments
|
||||||
|
|
||||||
GraphQL allows implementers (both objects and other interfaces) to return "subtypes" instead of an original value. Basically, this allows you to impose additional bounds on the implementation.
|
[GraphQL] allows implementers (both [objects][10] and other [interfaces][0]) to return "subtypes" instead of an original value. Basically, this allows to impose additional bounds on the implementation.
|
||||||
|
|
||||||
Valid "subtypes" are:
|
Valid "subtypes" are:
|
||||||
- interface implementer instead of an interface itself:
|
- [interface][0] implementer instead of an [interface][0] itself:
|
||||||
- `I implements T` in place of a `T`;
|
- `I implements T` in place of a `T`;
|
||||||
- `Vec<I implements T>` in place of a `Vec<T>`.
|
- `Vec<I implements T>` in place of a `Vec<T>`.
|
||||||
- non-null value in place of a nullable:
|
- [non-`null`][6] value in place of a `null`able:
|
||||||
- `T` in place of a `Option<T>`;
|
- `T` in place of a `Option<T>`;
|
||||||
- `Vec<T>` in place of a `Vec<Option<T>>`.
|
- `Vec<T>` in place of a `Vec<Option<T>>`.
|
||||||
|
|
||||||
These rules are recursively applied, so `Vec<Vec<I implements T>>` is a valid "subtype" of a `Option<Vec<Option<Vec<Option<T>>>>>`.
|
These rules are recursively applied, so `Vec<Vec<I implements T>>` is a valid "subtype" of a `Option<Vec<Option<Vec<Option<T>>>>>`.
|
||||||
|
|
||||||
Also, GraphQL allows implementers to add `null`able fields, which aren't present on an original interface.
|
Also, [GraphQL] allows implementers to add `null`able [field arguments][5], which aren't present on an original interface.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
use juniper::{graphql_interface, graphql_object, ID};
|
# use juniper::{graphql_interface, graphql_object, GraphQLInterface, ID};
|
||||||
|
#
|
||||||
#[graphql_interface(for = [HumanValue, Luke])]
|
#[derive(GraphQLInterface)]
|
||||||
|
#[graphql(for = [HumanValue, Luke])]
|
||||||
struct Node {
|
struct Node {
|
||||||
id: ID,
|
id: ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[graphql_interface(for = HumanConnectionValue)]
|
#[derive(GraphQLInterface)]
|
||||||
|
#[graphql(for = HumanConnectionValue)]
|
||||||
struct Connection {
|
struct Connection {
|
||||||
nodes: Vec<NodeValue>,
|
nodes: Vec<NodeValue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[graphql_interface(impl = NodeValue, for = Luke)]
|
#[derive(GraphQLInterface)]
|
||||||
|
#[graphql(impl = NodeValue, for = Luke)]
|
||||||
struct Human {
|
struct Human {
|
||||||
id: ID,
|
id: ID,
|
||||||
home_planet: String,
|
home_planet: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[graphql_interface(impl = ConnectionValue)]
|
#[derive(GraphQLInterface)]
|
||||||
|
#[graphql(impl = ConnectionValue)]
|
||||||
struct HumanConnection {
|
struct HumanConnection {
|
||||||
nodes: Vec<HumanValue>,
|
nodes: Vec<HumanValue>,
|
||||||
// ^^^^^^^^^^ notice not `NodeValue`
|
// ^^^^^^^^^^ notice not `NodeValue`
|
||||||
// This can happen, because every `Human` is a `Node` too, so we are just
|
// This can happen, because every `Human` is a `Node` too, so we just
|
||||||
// imposing additional bounds, which still can be resolved with
|
// impose additional bounds, which still can be resolved with
|
||||||
// `... on Connection { nodes }`.
|
// `... on Connection { nodes }` syntax.
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Luke {
|
struct Luke {
|
||||||
id: ID,
|
id: ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[graphql_object(impl = [HumanValue, NodeValue])]
|
#[graphql_object]
|
||||||
|
#[graphql(impl = [HumanValue, NodeValue])]
|
||||||
impl Luke {
|
impl Luke {
|
||||||
fn id(&self) -> &ID {
|
fn id(&self) -> &ID {
|
||||||
&self.id
|
&self.id
|
||||||
|
@ -197,13 +174,13 @@ impl Luke {
|
||||||
|
|
||||||
fn home_planet(language: Option<String>) -> &'static str {
|
fn home_planet(language: Option<String>) -> &'static str {
|
||||||
// ^^^^^^^^^^^^^^
|
// ^^^^^^^^^^^^^^
|
||||||
// Notice additional `null`able field, which is missing on `Human`.
|
// Notice additional `null`able field argument, which is missing on
|
||||||
// Resolving `...on Human { homePlanet }` will provide `None` for this
|
// `Human`. Resolving `...on Human { homePlanet }` will provide `None`
|
||||||
// argument.
|
// for this argument (default argument value).
|
||||||
match language.as_deref() {
|
match language.as_deref() {
|
||||||
None | Some("en") => "Tatooine",
|
None | Some("en") => "Tatooine",
|
||||||
Some("ko") => "타투인",
|
Some("ko") => "타투인",
|
||||||
_ => todo!(),
|
_ => unimplemented!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,271 +188,229 @@ impl Luke {
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
Violating GraphQL "subtyping" or additional nullable field rules is a compile-time error.
|
> **NOTE**: Violating [GraphQL] "subtyping" or additional `null`able [argument][5] rules is a **compile-time error**.
|
||||||
|
>
|
||||||
```compile_fail
|
> ```rust,compile_fail
|
||||||
# extern crate juniper;
|
> # extern crate juniper;
|
||||||
use juniper::{graphql_interface, graphql_object};
|
> # use juniper::{graphql_object, GraphQLInterface};
|
||||||
|
> #
|
||||||
pub struct ObjA {
|
> pub struct ObjA {
|
||||||
id: String,
|
> id: String,
|
||||||
}
|
> }
|
||||||
|
>
|
||||||
#[graphql_object(impl = CharacterValue)]
|
> #[graphql_object]
|
||||||
impl ObjA {
|
> #[graphql(impl = CharacterValue)]
|
||||||
fn id(&self, is_present: bool) -> &str {
|
> impl ObjA {
|
||||||
// ^^ the evaluated program panicked at
|
> fn id(&self, is_present: bool) -> &str {
|
||||||
// 'Failed to implement interface `Character` on `ObjA`: Field `id`: Argument `isPresent` of type `Boolean!`
|
> // ^^ the evaluated program panicked at
|
||||||
// isn't present on the interface and so has to be nullable.'
|
> // 'Failed to implement interface `Character` on `ObjA`: Field `id`: Argument
|
||||||
is_present.then_some(&self.id).unwrap_or("missing")
|
> // `isPresent` of type `Boolean!` isn't present on the interface and so has
|
||||||
}
|
> // to be nullable.'
|
||||||
}
|
> is_present.then_some(&self.id).unwrap_or("missing")
|
||||||
|
> }
|
||||||
#[graphql_interface(for = ObjA)]
|
> }
|
||||||
struct Character {
|
>
|
||||||
id: String,
|
> #[derive(GraphQLInterface)]
|
||||||
}
|
> #[graphql(for = ObjA)]
|
||||||
#
|
> struct Character {
|
||||||
# fn main() {}
|
> id: String,
|
||||||
```
|
> }
|
||||||
|
> #
|
||||||
```compile_fail
|
> # fn main() {}
|
||||||
# extern crate juniper;
|
> ```
|
||||||
use juniper::{graphql_interface, GraphQLObject};
|
>
|
||||||
|
> ```rust,compile_fail
|
||||||
#[derive(GraphQLObject)]
|
> # extern crate juniper;
|
||||||
#[graphql(impl = CharacterValue)]
|
> # use juniper::{GraphQLInterface, GraphQLObject};
|
||||||
pub struct ObjA {
|
> #
|
||||||
id: Vec<String>,
|
> #[derive(GraphQLObject)]
|
||||||
// ^^ the evaluated program panicked at
|
> #[graphql(impl = CharacterValue)]
|
||||||
// 'Failed to implement interface `Character` on `ObjA`: Field `id`: implementor is expected to return a subtype of
|
> pub struct ObjA {
|
||||||
// interface's return object: `[String!]!` is not a subtype of `String!`.'
|
> id: Vec<String>,
|
||||||
}
|
> // ^^ the evaluated program panicked at
|
||||||
|
> // 'Failed to implement interface `Character` on `ObjA`: Field `id`:
|
||||||
#[graphql_interface(for = ObjA)]
|
> // implementer is expected to return a subtype of interface's return
|
||||||
struct Character {
|
> // object: `[String!]!` is not a subtype of `String!`.'
|
||||||
id: String,
|
> }
|
||||||
}
|
>
|
||||||
#
|
> #[derive(GraphQLInterface)]
|
||||||
# fn main() {}
|
> #[graphql(for = ObjA)]
|
||||||
```
|
> struct Character {
|
||||||
|
> id: String,
|
||||||
|
> }
|
||||||
|
> #
|
||||||
|
> # fn main() {}
|
||||||
|
> ```
|
||||||
|
|
||||||
|
|
||||||
### Ignoring trait methods
|
### Default arguments
|
||||||
|
|
||||||
We may want to omit some trait methods to be assumed as [GraphQL interface][1] fields and ignore them.
|
|
||||||
|
|
||||||
|
[Similarly to GraphQL object fields](objects/complex_fields.md#default-arguments), [GraphQL arguments][4] of [interfaces][0] are able to have default values, though [Rust] doesn't have such notion:
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
use juniper::{graphql_interface, GraphQLObject};
|
# use juniper::graphql_interface;
|
||||||
|
#
|
||||||
#[graphql_interface(for = Human)]
|
#[graphql_interface]
|
||||||
trait Character {
|
trait Person {
|
||||||
fn id(&self) -> &str;
|
fn field1(
|
||||||
|
// Default value can be any valid Rust expression, including a function
|
||||||
#[graphql(ignore)] // or `#[graphql(skip)]`, your choice
|
// call, etc.
|
||||||
fn ignored(&self) -> u32 { 0 }
|
#[graphql(default = true)]
|
||||||
}
|
arg1: bool,
|
||||||
|
// If default expression is not specified, then the `Default::default()`
|
||||||
#[derive(GraphQLObject)]
|
// value is used.
|
||||||
#[graphql(impl = CharacterValue)]
|
#[graphql(default)]
|
||||||
struct Human {
|
arg2: i32,
|
||||||
id: String,
|
) -> String;
|
||||||
}
|
}
|
||||||
#
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Fields, arguments and interface customization
|
### Renaming
|
||||||
|
|
||||||
Similarly to [GraphQL objects][5] Juniper allows to fully customize [interface][1] fields and their arguments.
|
Just as with [defining GraphQL objects](objects/index.md#renaming), by default, [fields][4] are converted from [Rust]'s standard `snake_case` naming convention into [GraphQL]'s `camelCase` convention:
|
||||||
|
|
||||||
|
We can override the name by using the `#[graphql(name = "...")]` attribute:
|
||||||
```rust
|
```rust
|
||||||
# #![allow(deprecated)]
|
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
use juniper::graphql_interface;
|
# use juniper::{graphql_interface, GraphQLInterface};
|
||||||
|
#
|
||||||
|
#[derive(GraphQLInterface)]
|
||||||
|
#[graphql(name = "CharacterInterface")]
|
||||||
|
struct Character { // exposed as `CharacterInterface` in GraphQL schema
|
||||||
|
#[graphql(name = "myCustomFieldName")]
|
||||||
|
renamed_field: bool, // exposed as `myCustomFieldName` in GraphQL schema
|
||||||
|
}
|
||||||
|
|
||||||
// Renames the interface in GraphQL schema.
|
#[graphql_interface]
|
||||||
#[graphql_interface(name = "MyCharacter")]
|
#[graphql(name = "PersonInterface")]
|
||||||
// Describes the interface in GraphQL schema.
|
trait Person { // exposed as `PersonInterface` in GraphQL schema
|
||||||
#[graphql_interface(description = "My own character.")]
|
#[graphql(name = "myCustomFieldName")]
|
||||||
// Usual Rust docs are supported too as GraphQL interface description,
|
fn renamed_field( // exposed as `myCustomFieldName` in GraphQL schema
|
||||||
// but `description` attribute argument takes precedence over them, if specified.
|
#[graphql(name = "myArgument")]
|
||||||
/// This doc is absent in GraphQL schema.
|
renamed_argument: bool, // exposed as `myArgument` in GraphQL schema
|
||||||
trait Character {
|
) -> bool;
|
||||||
// Renames the field in GraphQL schema.
|
|
||||||
#[graphql(name = "myId")]
|
|
||||||
// Deprecates the field in GraphQL schema.
|
|
||||||
// Usual Rust `#[deprecated]` attribute is supported too as field deprecation,
|
|
||||||
// but `deprecated` attribute argument takes precedence over it, if specified.
|
|
||||||
#[graphql(deprecated = "Do not use it.")]
|
|
||||||
// Describes the field in GraphQL schema.
|
|
||||||
#[graphql(description = "ID of my own character.")]
|
|
||||||
// Usual Rust docs are supported too as field description,
|
|
||||||
// but `description` attribute argument takes precedence over them, if specified.
|
|
||||||
/// This description is absent in GraphQL schema.
|
|
||||||
fn id(
|
|
||||||
&self,
|
|
||||||
// Renames the argument in GraphQL schema.
|
|
||||||
#[graphql(name = "myNum")]
|
|
||||||
// Describes the argument in GraphQL schema.
|
|
||||||
#[graphql(description = "ID number of my own character.")]
|
|
||||||
// Specifies the default value for the argument.
|
|
||||||
// The concrete value may be omitted, and the `Default::default` one
|
|
||||||
// will be used in such case.
|
|
||||||
#[graphql(default = 5)]
|
|
||||||
num: i32,
|
|
||||||
) -> &str;
|
|
||||||
}
|
}
|
||||||
#
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
Renaming policies for all [GraphQL interface][1] fields and arguments are supported as well:
|
Or provide a different renaming policy for all the defined [fields][4]:
|
||||||
```rust
|
```rust
|
||||||
# #![allow(deprecated)]
|
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
use juniper::graphql_interface;
|
# use juniper::graphql_interface;
|
||||||
|
#
|
||||||
#[graphql_interface(rename_all = "none")] // disables any renaming
|
#[graphql_interface]
|
||||||
trait Character {
|
#[graphql(rename_all = "none")] // disables any renaming
|
||||||
// Now exposed as `my_id` and `my_num` in the schema
|
trait Person {
|
||||||
fn my_id(&self, my_num: i32) -> &str;
|
fn renamed_field( // exposed as `renamed_field` in GraphQL schema
|
||||||
|
renamed_argument: bool, // exposed as `renamed_argument` in GraphQL schema
|
||||||
|
) -> bool;
|
||||||
}
|
}
|
||||||
#
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming).
|
||||||
|
|
||||||
|
|
||||||
### Custom context
|
### Documentation and deprecation
|
||||||
|
|
||||||
If a [`Context`][6] is required in a trait method to resolve a [GraphQL interface][1] field, specify it as an argument.
|
|
||||||
|
|
||||||
|
Similarly, [GraphQL fields][4] of [interfaces][0] may also be [documented][7] and [deprecated][9] via `#[graphql(description = "...")]` and `#[graphql(deprecated = "...")]`/[`#[deprecated]`][13] attributes:
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
# use std::collections::HashMap;
|
# use juniper::graphql_interface;
|
||||||
use juniper::{graphql_interface, GraphQLObject};
|
|
||||||
|
|
||||||
struct Database {
|
|
||||||
humans: HashMap<String, Human>,
|
|
||||||
}
|
|
||||||
impl juniper::Context for Database {}
|
|
||||||
|
|
||||||
#[graphql_interface(for = Human)] // look, ma, context type is inferred! \(^o^)/
|
|
||||||
trait Character { // while still can be specified via `Context = ...` attribute argument
|
|
||||||
// If a field argument is named `context` or `ctx`, it's automatically assumed
|
|
||||||
// as a context argument.
|
|
||||||
fn id(&self, context: &Database) -> Option<&str>;
|
|
||||||
|
|
||||||
// Otherwise, you may mark it explicitly as a context argument.
|
|
||||||
fn name(&self, #[graphql(context)] db: &Database) -> Option<&str>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(impl = CharacterValue, Context = Database)]
|
|
||||||
struct Human {
|
|
||||||
id: String,
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
#
|
#
|
||||||
# fn main() {}
|
/// This doc comment is visible only in Rust API docs.
|
||||||
```
|
#[graphql_interface]
|
||||||
|
#[graphql(description = "This description overwrites the one from doc comment.")]
|
||||||
|
trait Person {
|
||||||
### Using executor and explicit generic scalar
|
/// This doc comment is visible only in Rust API docs.
|
||||||
|
#[graphql(description = "This description is visible only in GraphQL schema.")]
|
||||||
If an [`Executor`][4] is required in a trait method to resolve a [GraphQL interface][1] field, specify it as an argument.
|
fn empty() -> &'static str;
|
||||||
|
|
||||||
This requires to explicitly parametrize over [`ScalarValue`][3], as [`Executor`][4] does so.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
use juniper::{graphql_interface, graphql_object, Executor, LookAheadMethods as _, ScalarValue};
|
|
||||||
|
|
||||||
#[graphql_interface(for = Human, Scalar = S)] // notice specifying `ScalarValue` as existing type parameter
|
|
||||||
trait Character<S: ScalarValue> {
|
|
||||||
// If a field argument is named `executor`, it's automatically assumed
|
|
||||||
// as an executor argument.
|
|
||||||
fn id<'a>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str;
|
|
||||||
|
|
||||||
// Otherwise, you may mark it explicitly as an executor argument.
|
|
||||||
fn name<'b>(
|
|
||||||
&'b self,
|
|
||||||
#[graphql(executor)] another: &Executor<'_, '_, (), S>,
|
|
||||||
) -> &'b str;
|
|
||||||
|
|
||||||
fn home_planet(&self) -> &str;
|
#[graphql(desc = "This description is visible only in GraphQL schema.")]
|
||||||
}
|
// ^^^^ shortcut for a `description` argument
|
||||||
|
fn field(
|
||||||
|
#[graphql(desc = "This description is visible only in GraphQL schema.")]
|
||||||
|
arg: bool,
|
||||||
|
) -> bool;
|
||||||
|
|
||||||
struct Human {
|
/// This doc comment is visible in both Rust API docs and GraphQL schema
|
||||||
id: String,
|
/// descriptions.
|
||||||
name: String,
|
#[graphql(deprecated = "Just because.")]
|
||||||
home_planet: String,
|
fn deprecated_graphql() -> bool;
|
||||||
}
|
|
||||||
#[graphql_object(scalar = S: ScalarValue, impl = CharacterValue<S>)]
|
|
||||||
impl Human {
|
|
||||||
async fn id<'a, S>(&self, executor: &'a Executor<'_, '_, (), S>) -> &'a str
|
|
||||||
where
|
|
||||||
S: ScalarValue,
|
|
||||||
{
|
|
||||||
executor.look_ahead().field_name()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn name<'b, S>(&'b self, #[graphql(executor)] _: &Executor<'_, '_, (), S>) -> &'b str {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
fn home_planet<'c, S>(&'c self, #[graphql(executor)] _: &Executor<'_, '_, (), S>) -> &'c str {
|
// Standard Rust's `#[deprecated]` attribute works too!
|
||||||
// Executor may not be present on the trait method ^^^^^^^^^^^^^^^^^^^^^^^^
|
#[deprecated(note = "Reason is optional, btw!")]
|
||||||
&self.home_planet
|
fn deprecated_standard() -> bool; // has no description in GraphQL schema
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
> **NOTE**: Only [GraphQL interface][0]/[object][10] fields and [GraphQL enum][11] values can be [deprecated][9].
|
||||||
|
|
||||||
|
|
||||||
|
### Ignoring
|
||||||
|
|
||||||
|
By default, all [struct][24] fields or [trait][20] methods are considered as [GraphQL fields][4]. If a helper method is needed, or it should be ignored for some reason, then it should be marked with the `#[graphql(ignore)]` attribute:
|
||||||
## `ScalarValue` considerations
|
|
||||||
|
|
||||||
By default, `#[graphql_interface]` macro generates code, which is generic over a [`ScalarValue`][3] type. This may introduce a problem when at least one of [GraphQL interface][1] implementers is restricted to a concrete [`ScalarValue`][3] type in its implementation. To resolve such problem, a concrete [`ScalarValue`][3] type should be specified.
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
# #![allow(dead_code)]
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
use juniper::{graphql_interface, DefaultScalarValue, GraphQLObject};
|
# use std::marker::PhantomPinned;
|
||||||
|
# use juniper::{graphql_interface, GraphQLInterface};
|
||||||
#[graphql_interface(for = [Human, Droid])]
|
#
|
||||||
#[graphql_interface(scalar = DefaultScalarValue)] // removing this line will fail compilation
|
#[derive(GraphQLInterface)]
|
||||||
trait Character {
|
struct Character {
|
||||||
fn id(&self) -> &str;
|
id: i32,
|
||||||
|
#[graphql(ignore)]
|
||||||
|
_pin: PhantomPinned,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
#[graphql_interface]
|
||||||
#[graphql(impl = CharacterValue, Scalar = DefaultScalarValue)]
|
trait Person {
|
||||||
struct Human {
|
fn name(&self) -> &str;
|
||||||
id: String,
|
|
||||||
home_planet: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
fn age(&self) -> i32;
|
||||||
#[graphql(impl = CharacterValue, Scalar = DefaultScalarValue)]
|
|
||||||
struct Droid {
|
#[graphql(ignore)]
|
||||||
id: String,
|
fn hidden_from_graphql(&self) {
|
||||||
primary_function: String,
|
// Ignored methods are allowed to have a default implementation!
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql(skip)]
|
||||||
|
// ^^^^ alternative naming, up to your preference
|
||||||
|
fn also_hidden_from_graphql(&self);
|
||||||
}
|
}
|
||||||
#
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **TIP**: See more available features in the API docs of the [`#[graphql_interface]`][3] attribute.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[1]: https://spec.graphql.org/October2021#sec-Interfaces
|
[GraphQL]: https://graphql.org
|
||||||
[2]: https://doc.rust-lang.org/reference/types/trait-object.html
|
[Juniper]: https://docs.rs/juniper
|
||||||
[3]: https://docs.rs/juniper/latest/juniper/trait.ScalarValue.html
|
[Rust]: https://www.rust-lang.org
|
||||||
[4]: https://docs.rs/juniper/latest/juniper/struct.Executor.html
|
|
||||||
[5]: https://spec.graphql.org/October2021#sec-Objects
|
[0]: https://spec.graphql.org/October2021#sec-Interfaces
|
||||||
[6]: https://docs.rs/juniper/0.14.2/juniper/trait.Context.html
|
[2]: https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLInterface.html
|
||||||
|
[3]: https://docs.rs/juniper/0.16.1/juniper/attr.graphql_interface.html
|
||||||
|
[4]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||||
|
[5]: https://spec.graphql.org/October2021#sec-Language.Arguments
|
||||||
|
[6]: https://spec.graphql.org/October2021#sec-Non-Null
|
||||||
|
[7]: https://spec.graphql.org/October2021#sec-Descriptions
|
||||||
|
[9]: https://spec.graphql.org/October2021#sec--deprecated
|
||||||
|
[10]: https://spec.graphql.org/October2021#sec-Objects
|
||||||
|
[11]: https://spec.graphql.org/October2021#sec-Enums
|
||||||
|
[13]: https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-deprecated-attribute
|
||||||
|
[20]: https://doc.rust-lang.org/reference/items/traits.html#traits
|
||||||
|
[21]: https://doc.rust-lang.org/reference/types/trait-object.html#trait-objects
|
||||||
|
[22]: https://doc.rust-lang.org/reference/items/enumerations.html#enumerations
|
||||||
|
[23]: https://doc.rust-lang.org/reference/items/implementations.html#trait-implementations
|
||||||
|
[24]: https://doc.rust-lang.org/reference/items/structs.html
|
||||||
|
[30]: https://en.wikipedia.org/wiki/Structural_type_system
|
||||||
|
[31]: https://en.wikipedia.org/wiki/Type_class
|
||||||
|
|
|
@ -1,15 +1,169 @@
|
||||||
# Complex fields
|
Complex fields
|
||||||
|
==============
|
||||||
|
|
||||||
If you've got a struct that can't be mapped directly to GraphQL, that contains
|
Using a plain [Rust struct][struct] for representing a [GraphQL object][0] is easy and trivial but does not cover every case. What if we need to express something non-trivial as a [GraphQL field][4], such as:
|
||||||
computed fields or circular structures, you have to use a more powerful tool:
|
- Calling non-trivial logic while [executing][1] the [field][4] (like querying database, etc.).
|
||||||
the `#[graphql_object]` procedural macro. This macro lets you define GraphQL object
|
- Accepting [field arguments][5].
|
||||||
fields in a Rust `impl` block for a type. Note, that GraphQL fields are defined in
|
- Defining a circular [GraphQL object][0], where one of its [fields][4] returns the type itself.
|
||||||
this `impl` block by default. If you want to define normal methods on the struct,
|
- Using some other (non-[struct]) [Rust] type to represent a [GraphQL object][0].
|
||||||
you have to do so either in a separate "normal" `impl` block, or mark them with
|
|
||||||
`#[graphql(ignore)]` attribute to be omitted by the macro. Continuing with the
|
|
||||||
example from the last chapter, this is how you would define `Person` using the
|
|
||||||
macro:
|
|
||||||
|
|
||||||
|
To support these more complicated use cases, we need a way to define a [GraphQL field][4] as a function. In [Juniper] this is achievable by placing the [`#[graphql_object]` attribute][3] on an [`impl` block][6], which turns its methods into [GraphQL fields][4]:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::{graphql_object, GraphQLObject};
|
||||||
|
#
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
struct Person {
|
||||||
|
name: String,
|
||||||
|
age: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct House {
|
||||||
|
inhabitants: Vec<Person>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defines the `House` GraphQL object.
|
||||||
|
#[graphql_object]
|
||||||
|
impl House {
|
||||||
|
// Creates the field `inhabitantWithName(name: String!)`,
|
||||||
|
// returning a `null`able `Person`.
|
||||||
|
fn inhabitant_with_name(&self, name: String) -> Option<&Person> {
|
||||||
|
self.inhabitants.iter().find(|p| p.name == name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
> **NOTE**: To access global data such as database connections or authentication information, a _context_ is used. To learn more about this, see the ["Context" chapter](context.md).
|
||||||
|
|
||||||
|
|
||||||
|
### Default arguments
|
||||||
|
|
||||||
|
Though [Rust] doesn't have the notion of default arguments, [GraphQL arguments][4] are able to have default values. These default values are used when a GraphQL operation doesn't specify the argument explicitly. In [Juniper], defining a default value for a [GraphQL argument][4] is enabled by the `#[graphql(default)]` attribute:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::graphql_object;
|
||||||
|
#
|
||||||
|
struct Person;
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
impl Person {
|
||||||
|
fn field1(
|
||||||
|
// Default value can be any valid Rust expression, including a function
|
||||||
|
// call, etc.
|
||||||
|
#[graphql(default = true)]
|
||||||
|
arg1: bool,
|
||||||
|
// If default expression is not specified, then the `Default::default()`
|
||||||
|
// value is used.
|
||||||
|
#[graphql(default)]
|
||||||
|
arg2: i32,
|
||||||
|
) -> String {
|
||||||
|
format!("{arg1} {arg2}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Renaming
|
||||||
|
|
||||||
|
Like with the [`#[derive(GraphQLObject)]` attribute on structs](index.md#renaming), [field][4] names are converted from [Rust]'s standard `snake_case` naming convention into [GraphQL]'s `camelCase` convention.
|
||||||
|
|
||||||
|
We can override the name by using the `#[graphql(name = "...")]` attribute:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::graphql_object;
|
||||||
|
#
|
||||||
|
struct Person;
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
#[graphql(name = "PersonObject")]
|
||||||
|
impl Person { // exposed as `PersonObject` in GraphQL schema
|
||||||
|
#[graphql(name = "myCustomFieldName")]
|
||||||
|
fn renamed_field( // exposed as `myCustomFieldName` in GraphQL schema
|
||||||
|
#[graphql(name = "myArgument")]
|
||||||
|
renamed_argument: bool, // exposed as `myArgument` in GraphQL schema
|
||||||
|
) -> bool {
|
||||||
|
renamed_argument
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or provide a different renaming policy for all the defined [fields][4]:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::graphql_object;
|
||||||
|
#
|
||||||
|
struct Person;
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
#[graphql(rename_all = "none")] // disables any renaming
|
||||||
|
impl Person {
|
||||||
|
fn renamed_field( // exposed as `renamed_field` in GraphQL schema
|
||||||
|
renamed_argument: bool, // exposed as `renamed_argument` in GraphQL schema
|
||||||
|
) -> bool {
|
||||||
|
renamed_argument
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming).
|
||||||
|
|
||||||
|
|
||||||
|
### Documentation and deprecation
|
||||||
|
|
||||||
|
Similarly, [GraphQL fields][4] may also be [documented][7] and [deprecated][9] via `#[graphql(description = "...")]` and `#[graphql(deprecated = "...")]`/[`#[deprecated]`][13] attributes:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::graphql_object;
|
||||||
|
#
|
||||||
|
struct Person;
|
||||||
|
|
||||||
|
/// This doc comment is visible only in Rust API docs.
|
||||||
|
#[graphql_object]
|
||||||
|
#[graphql(description = "This description overwrites the one from doc comment.")]
|
||||||
|
impl Person {
|
||||||
|
/// This doc comment is visible only in Rust API docs.
|
||||||
|
#[graphql(description = "This description is visible only in GraphQL schema.")]
|
||||||
|
fn empty() -> &'static str {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql(desc = "This description is visible only in GraphQL schema.")]
|
||||||
|
// ^^^^ shortcut for a `description` argument
|
||||||
|
fn field(
|
||||||
|
#[graphql(desc = "This description is visible only in GraphQL schema.")]
|
||||||
|
arg: bool,
|
||||||
|
) -> bool {
|
||||||
|
arg
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This doc comment is visible in both Rust API docs and GraphQL schema
|
||||||
|
/// descriptions.
|
||||||
|
#[graphql(deprecated = "Just because.")]
|
||||||
|
fn deprecated_graphql() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard Rust's `#[deprecated]` attribute works too!
|
||||||
|
#[deprecated(note = "Reason is optional, btw!")]
|
||||||
|
fn deprecated_standard() -> bool { // has no description in GraphQL schema
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
> **NOTE**: Only [GraphQL object][0]/[interface][11] fields and [GraphQL enum][10] values can be [deprecated][9].
|
||||||
|
|
||||||
|
|
||||||
|
### Ignoring
|
||||||
|
|
||||||
|
By default, all methods of an [`impl` block][6] are exposed as [GraphQL fields][4]. If a method should not be exposed as a [GraphQL field][4], it should be defined in a separate [`impl` block][6] or marked with the `#[graphql(ignore)]` attribute:
|
||||||
```rust
|
```rust
|
||||||
# #![allow(dead_code)]
|
# #![allow(dead_code)]
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
|
@ -32,175 +186,44 @@ impl Person {
|
||||||
|
|
||||||
#[graphql(ignore)]
|
#[graphql(ignore)]
|
||||||
pub fn hidden_from_graphql(&self) {
|
pub fn hidden_from_graphql(&self) {
|
||||||
// [...]
|
// whatever goes...
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql(skip)]
|
||||||
|
// ^^^^ alternative naming, up to your preference
|
||||||
|
pub fn also_hidden_from_graphql(&self) {
|
||||||
|
// whatever goes...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Person {
|
impl Person {
|
||||||
pub fn hidden_from_graphql2(&self) {
|
pub fn not_even_considered_for_graphql(&self) {
|
||||||
// [...]
|
// whatever goes...
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() { }
|
|
||||||
```
|
|
||||||
|
|
||||||
While this is a bit more verbose, it lets you write any kind of function in the
|
|
||||||
field resolver. With this syntax, fields can also take arguments:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::{graphql_object, GraphQLObject};
|
|
||||||
#
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
struct Person {
|
|
||||||
name: String,
|
|
||||||
age: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct House {
|
|
||||||
inhabitants: Vec<Person>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[graphql_object]
|
|
||||||
impl House {
|
|
||||||
// Creates the field `inhabitantWithName(name)`, returning a nullable `Person`.
|
|
||||||
fn inhabitant_with_name(&self, name: String) -> Option<&Person> {
|
|
||||||
self.inhabitants.iter().find(|p| p.name == name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
To access global data such as database connections or authentication
|
> **TIP**: See more available features in the API docs of the [`#[graphql_object]`][3] attribute.
|
||||||
information, a _context_ is used. To learn more about this, see the next
|
|
||||||
chapter: [Using contexts](using_contexts.md).
|
|
||||||
|
|
||||||
## Description, renaming, and deprecation
|
|
||||||
|
|
||||||
Like with the derive attribute, field names will be converted from `snake_case`
|
|
||||||
to `camelCase`. If you need to override the conversion, you can simply rename
|
|
||||||
the field. Also, the type name can be changed with an alias:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::graphql_object;
|
|
||||||
#
|
|
||||||
struct Person;
|
|
||||||
|
|
||||||
/// Doc comments are used as descriptions for GraphQL.
|
[GraphQL]: https://graphql.org
|
||||||
#[graphql_object(
|
[Juniper]: https://docs.rs/juniper
|
||||||
// With this attribute you can change the public GraphQL name of the type.
|
[Rust]: https://www.rust-lang.org
|
||||||
name = "PersonObject",
|
[struct]: https://doc.rust-lang.org/reference/items/structs.html
|
||||||
|
|
||||||
// You can also specify a description here, which will overwrite
|
[0]: https://spec.graphql.org/October2021#sec-Objects
|
||||||
// a doc comment description.
|
[1]: https://spec.graphql.org/October2021#sec-Execution
|
||||||
description = "...",
|
[2]: https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLObject.html
|
||||||
)]
|
[3]: https://docs.rs/juniper/0.16.1/juniper/attr.graphql_object.html
|
||||||
impl Person {
|
[4]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||||
/// A doc comment on the field will also be used for GraphQL.
|
[5]: https://spec.graphql.org/October2021#sec-Language.Arguments
|
||||||
#[graphql(
|
[6]: https://doc.rust-lang.org/reference/items/implementations.html#inherent-implementations
|
||||||
// Or provide a description here.
|
[7]: https://spec.graphql.org/October2021#sec-Descriptions
|
||||||
description = "...",
|
[9]: https://spec.graphql.org/October2021#sec--deprecated
|
||||||
)]
|
[10]: https://spec.graphql.org/October2021#sec-Enums
|
||||||
fn doc_comment(&self) -> &str {
|
[11]: https://spec.graphql.org/October2021#sec-Interfaces
|
||||||
""
|
[13]: https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-deprecated-attribute
|
||||||
}
|
|
||||||
|
|
||||||
// Fields can also be renamed if required.
|
|
||||||
#[graphql(name = "myCustomFieldName")]
|
|
||||||
fn renamed_field() -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecations also work as you'd expect.
|
|
||||||
// Both the standard Rust syntax and a custom attribute is accepted.
|
|
||||||
#[deprecated(note = "...")]
|
|
||||||
fn deprecated_standard() -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
#[graphql(deprecated = "...")]
|
|
||||||
fn deprecated_graphql() -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() { }
|
|
||||||
```
|
|
||||||
|
|
||||||
Or provide a different renaming policy on a `impl` block for all its fields:
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::graphql_object;
|
|
||||||
struct Person;
|
|
||||||
|
|
||||||
#[graphql_object(rename_all = "none")] // disables any renaming
|
|
||||||
impl Person {
|
|
||||||
// Now exposed as `renamed_field` in the schema
|
|
||||||
fn renamed_field() -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Customizing arguments
|
|
||||||
|
|
||||||
Method field arguments can also be customized.
|
|
||||||
|
|
||||||
They can have custom descriptions and default values.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::graphql_object;
|
|
||||||
#
|
|
||||||
struct Person;
|
|
||||||
|
|
||||||
#[graphql_object]
|
|
||||||
impl Person {
|
|
||||||
fn field1(
|
|
||||||
&self,
|
|
||||||
#[graphql(
|
|
||||||
// Arguments can also be renamed if required.
|
|
||||||
name = "arg",
|
|
||||||
// Set a default value which will be injected if not present.
|
|
||||||
// The default can be any valid Rust expression, including a function call, etc.
|
|
||||||
default = true,
|
|
||||||
// Set a description.
|
|
||||||
description = "The first argument..."
|
|
||||||
)]
|
|
||||||
arg1: bool,
|
|
||||||
// If default expression is not specified then `Default::default()` value is used.
|
|
||||||
#[graphql(default)]
|
|
||||||
arg2: i32,
|
|
||||||
) -> String {
|
|
||||||
format!("{arg1} {arg2}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() { }
|
|
||||||
```
|
|
||||||
|
|
||||||
Provide a different renaming policy on a `impl` block also implies for arguments:
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::graphql_object;
|
|
||||||
struct Person;
|
|
||||||
|
|
||||||
#[graphql_object(rename_all = "none")] // disables any renaming
|
|
||||||
impl Person {
|
|
||||||
// Now exposed as `my_arg` in the schema
|
|
||||||
fn field(my_arg: bool) -> bool {
|
|
||||||
my_arg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
## More features
|
|
||||||
|
|
||||||
These, and more features, are described more thoroughly in [the reference documentation](https://docs.rs/juniper/latest/juniper/attr.graphql_object.html).
|
|
||||||
|
|
156
book/src/types/objects/context.md
Normal file
156
book/src/types/objects/context.md
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
Context
|
||||||
|
=======
|
||||||
|
|
||||||
|
_Context_ is a feature in [Juniper] that lets [field][4] resolvers access global data, most commonly database connections or authentication information.
|
||||||
|
|
||||||
|
Let's say that we have a simple `User`s database in a `HashMap`:
|
||||||
|
```rust
|
||||||
|
# #![allow(dead_code)]
|
||||||
|
# use std::collections::HashMap;
|
||||||
|
#
|
||||||
|
struct Database {
|
||||||
|
users: HashMap<i32, User>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct User {
|
||||||
|
id: i32,
|
||||||
|
name: String,
|
||||||
|
friend_ids: Vec<i32>,
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
We would like to define a `friends` [field][4] on `User` that returns a list of `User` [objects][0]. In order to write such a [field][4] we need to query a `Database`. To accomplish this we must first mark the `Database` as a valid context type and then assign it to the `User` [object][0]. To gain access to the context in the `friends` [field][4], we need to specify an argument with the same type as the specified context:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use std::collections::HashMap;
|
||||||
|
# use juniper::graphql_object;
|
||||||
|
#
|
||||||
|
struct Database {
|
||||||
|
users: HashMap<i32, User>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark the `Database` as a valid context type for Juniper.
|
||||||
|
impl juniper::Context for Database {}
|
||||||
|
|
||||||
|
struct User {
|
||||||
|
id: i32,
|
||||||
|
name: String,
|
||||||
|
friend_ids: Vec<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
#[graphql(context = Database)] // assign `Database` as the context type
|
||||||
|
impl User {
|
||||||
|
// Inject the `Database` context by specifying an argument with the
|
||||||
|
// context type:
|
||||||
|
// - the type must be a reference;
|
||||||
|
// - the name of the argument SHOULD be `context` (or `ctx`).
|
||||||
|
fn friends<'db>(&self, context: &'db Database) -> Vec<&'db User> {
|
||||||
|
// ^^^^^^^ or `ctx`, up to your preference
|
||||||
|
self.friend_ids.iter()
|
||||||
|
.map(|id| {
|
||||||
|
context.users.get(&id).expect("could not find `User` with ID")
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn friend<'db>(
|
||||||
|
&self,
|
||||||
|
id: i32,
|
||||||
|
// Alternatively, the context argument may be marked with an attribute,
|
||||||
|
// and thus, named arbitrary.
|
||||||
|
#[graphql(context)] db: &'db Database,
|
||||||
|
// ^^^^^^^ or `ctx`, up to your preference
|
||||||
|
) -> Option<&'db User> {
|
||||||
|
self.friend_ids.contains(&id).then(|| {
|
||||||
|
db.users.get(&id).expect("could not find `User` with ID")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
self.name.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> i32 {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Mutating and mutable references
|
||||||
|
|
||||||
|
Context cannot be a mutable reference as [fields][4] may be resolved concurrently. If something in the context requires a mutable reference, the context type should leverage the [_interior mutability_ pattern][5] (e.g. use `RwLock`, `RefCell` or similar).
|
||||||
|
|
||||||
|
For example, when using async runtime with [work stealing][6] (like [`tokio`]), which obviously requires thread safety in addition, we will need to use a corresponding async version of `RwLock`:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# extern crate tokio;
|
||||||
|
# use std::collections::HashMap;
|
||||||
|
# use juniper::graphql_object;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
struct Database {
|
||||||
|
requested_count: HashMap<i32, i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we cannot directly implement `juniper::Context`
|
||||||
|
// for `RwLock`, we use the newtype idiom.
|
||||||
|
struct DatabaseContext(RwLock<Database>);
|
||||||
|
|
||||||
|
impl juniper::Context for DatabaseContext {}
|
||||||
|
|
||||||
|
struct User {
|
||||||
|
id: i32,
|
||||||
|
name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
#[graphql(context = DatabaseContext)]
|
||||||
|
impl User {
|
||||||
|
async fn times_requested<'db>(&self, ctx: &'db DatabaseContext) -> i32 {
|
||||||
|
// Acquire a mutable reference and `.await` if async `RwLock` is used,
|
||||||
|
// which is necessary if context consists of async operations like
|
||||||
|
// querying remote databases.
|
||||||
|
|
||||||
|
// Obtain base type.
|
||||||
|
let DatabaseContext(db) = ctx;
|
||||||
|
// If context is immutable use `.read()` on `RwLock` instead.
|
||||||
|
let mut db = db.write().await;
|
||||||
|
|
||||||
|
// Perform a mutable operation.
|
||||||
|
db.requested_count
|
||||||
|
.entry(self.id)
|
||||||
|
.and_modify(|e| *e += 1)
|
||||||
|
.or_insert(1)
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
self.name.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> i32 {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
> **TIP**: Replace `tokio::sync::RwLock` with `std::sync::RwLock` (or similar) if you don't intend to use async resolving.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[`tokio`]: https://docs.rs/tokio
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
[Rust]: https://www.rust-lang.org
|
||||||
|
|
||||||
|
[0]: https://spec.graphql.org/October2021#sec-Objects
|
||||||
|
[4]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||||
|
[5]: https://doc.rust-lang.org/reference/interior-mutability.html#interior-mutability
|
||||||
|
[6]: https://en.wikipedia.org/wiki/Work_stealing
|
|
@ -1,216 +0,0 @@
|
||||||
# Defining objects
|
|
||||||
|
|
||||||
While any type in Rust can be exposed as a GraphQL object, the most common one
|
|
||||||
is a struct.
|
|
||||||
|
|
||||||
There are two ways to create a GraphQL object in Juniper. If you've got a simple
|
|
||||||
struct you want to expose, the easiest way is to use the custom derive
|
|
||||||
attribute. The other way is described in the [Complex fields](complex_fields.md)
|
|
||||||
chapter.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::GraphQLObject;
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
struct Person {
|
|
||||||
name: String,
|
|
||||||
age: i32,
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
This will create a GraphQL object type called `Person`, with two fields: `name`
|
|
||||||
of type `String!`, and `age` of type `Int!`. Because of Rust's type system,
|
|
||||||
everything is exported as non-null by default. If you need a nullable field, you
|
|
||||||
can use `Option<T>`.
|
|
||||||
|
|
||||||
We should take advantage of the
|
|
||||||
fact that GraphQL is self-documenting and add descriptions to the type and
|
|
||||||
fields. Juniper will automatically use associated doc comments as GraphQL
|
|
||||||
descriptions:
|
|
||||||
|
|
||||||
!FILENAME GraphQL descriptions via Rust doc comments
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::GraphQLObject;
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
/// Information about a person
|
|
||||||
struct Person {
|
|
||||||
/// The person's full name, including both first and last names
|
|
||||||
name: String,
|
|
||||||
/// The person's age in years, rounded down
|
|
||||||
age: i32,
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
Objects and fields without doc comments can instead set a `description`
|
|
||||||
via the `graphql` attribute. The following example is equivalent to the above:
|
|
||||||
|
|
||||||
!FILENAME GraphQL descriptions via attribute
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::GraphQLObject;
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(description = "Information about a person")]
|
|
||||||
struct Person {
|
|
||||||
#[graphql(description = "The person's full name, including both first and last names")]
|
|
||||||
name: String,
|
|
||||||
#[graphql(description = "The person's age in years, rounded down")]
|
|
||||||
age: i32,
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
Descriptions set via the `graphql` attribute take precedence over Rust
|
|
||||||
doc comments. This enables internal Rust documentation and external GraphQL
|
|
||||||
documentation to differ:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::GraphQLObject;
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(description = "This description shows up in GraphQL")]
|
|
||||||
/// This description shows up in RustDoc
|
|
||||||
struct Person {
|
|
||||||
#[graphql(description = "This description shows up in GraphQL")]
|
|
||||||
/// This description shows up in RustDoc
|
|
||||||
name: String,
|
|
||||||
/// This description shows up in both RustDoc and GraphQL
|
|
||||||
age: i32,
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Relationships
|
|
||||||
|
|
||||||
You can only use the custom derive attribute under these circumstances:
|
|
||||||
|
|
||||||
- The annotated type is a `struct`,
|
|
||||||
- Every struct field is either
|
|
||||||
- A primitive type (`i32`, `f64`, `bool`, `String`, `juniper::ID`), or
|
|
||||||
- A valid custom GraphQL type, e.g. another struct marked with this attribute,
|
|
||||||
or
|
|
||||||
- A container/reference containing any of the above, e.g. `Vec<T>`, `Box<T>`,
|
|
||||||
`Option<T>`
|
|
||||||
|
|
||||||
Let's see what that means for building relationships between objects:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::GraphQLObject;
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
struct Person {
|
|
||||||
name: String,
|
|
||||||
age: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
struct House {
|
|
||||||
address: Option<String>, // Converted into String (nullable)
|
|
||||||
inhabitants: Vec<Person>, // Converted into [Person!]!
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
Because `Person` is a valid GraphQL type, you can have a `Vec<Person>` in a
|
|
||||||
struct and it'll be automatically converted into a list of non-nullable `Person`
|
|
||||||
objects.
|
|
||||||
|
|
||||||
## Renaming fields
|
|
||||||
|
|
||||||
By default, struct fields are converted from Rust's standard `snake_case` naming
|
|
||||||
convention into GraphQL's `camelCase` convention:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::GraphQLObject;
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
struct Person {
|
|
||||||
first_name: String, // Would be exposed as firstName in the GraphQL schema
|
|
||||||
last_name: String, // Exposed as lastName
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
You can override the name by using the `graphql` attribute on individual struct
|
|
||||||
fields:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::GraphQLObject;
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
struct Person {
|
|
||||||
name: String,
|
|
||||||
age: i32,
|
|
||||||
#[graphql(name = "websiteURL")]
|
|
||||||
website_url: Option<String>, // now exposed as `websiteURL` in the schema
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
Or provide a different renaming policy on a struct for all its fields:
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::GraphQLObject;
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(rename_all = "none")] // disables any renaming
|
|
||||||
struct Person {
|
|
||||||
name: String,
|
|
||||||
age: i32,
|
|
||||||
website_url: Option<String>, // now exposed as `website_url` in the schema
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deprecating fields
|
|
||||||
|
|
||||||
To deprecate a field, you specify a deprecation reason using the `graphql`
|
|
||||||
attribute:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::GraphQLObject;
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
struct Person {
|
|
||||||
name: String,
|
|
||||||
age: i32,
|
|
||||||
#[graphql(deprecated = "Please use the name field instead")]
|
|
||||||
first_name: String,
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
The `name`, `description`, and `deprecation` arguments can of course be
|
|
||||||
combined. Some restrictions from the GraphQL spec still applies though; you can
|
|
||||||
only deprecate object fields and enum values.
|
|
||||||
|
|
||||||
## Ignoring fields
|
|
||||||
|
|
||||||
By default, all fields in a `GraphQLObject` are included in the generated GraphQL type. To prevent including a specific field, annotate the field with `#[graphql(ignore)]`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::GraphQLObject;
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
struct Person {
|
|
||||||
name: String,
|
|
||||||
age: i32,
|
|
||||||
#[graphql(ignore)]
|
|
||||||
# #[allow(dead_code)]
|
|
||||||
password_hash: String, // cannot be queried or modified from GraphQL
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
183
book/src/types/objects/error/field.md
Normal file
183
book/src/types/objects/error/field.md
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
Field errors
|
||||||
|
============
|
||||||
|
|
||||||
|
[Rust] provides [two ways of dealing with errors][11]:
|
||||||
|
- [`Result<T, E>`][12] for recoverable errors;
|
||||||
|
- [`panic!`][13] for unrecoverable errors.
|
||||||
|
|
||||||
|
[Juniper] does not do anything about panicking, it naturally bubbles up to the surrounding code/framework and can be dealt with there.
|
||||||
|
|
||||||
|
For recoverable errors, [Juniper] works well with the [built-in `Result` type][12]. You can use the [`?` operator][14] and things will work as you expect them to:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use std::{fs::File, io::Read, path::PathBuf, str};
|
||||||
|
# use juniper::{graphql_object, FieldResult};
|
||||||
|
#
|
||||||
|
struct Example {
|
||||||
|
filename: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
impl Example {
|
||||||
|
fn contents(&self) -> FieldResult<String> {
|
||||||
|
let mut file = File::open(&self.filename)?;
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents)?;
|
||||||
|
Ok(contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn foo() -> FieldResult<Option<String>> {
|
||||||
|
// Some invalid bytes.
|
||||||
|
let invalid = vec![128, 223];
|
||||||
|
|
||||||
|
Ok(Some(str::from_utf8(&invalid)?.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
[`FieldResult<T>`][21] is an alias for [`Result<T, FieldError>`][22], which is the [error type][1] all fallible [fields][6] must return. By using the [`?` operator][14], any type that implements the [`Display` trait][15] (which most of the error types out there do) can be automatically converted into a [`FieldError`][22].
|
||||||
|
|
||||||
|
> **TIP**: If a custom conversion into a [`FieldError`][22] is needed (to [fill up `extensions`][2], for example), the [`IntoFieldError` trait][23] should be implemented.
|
||||||
|
|
||||||
|
> **NOTE**: [`FieldError`][22]s are [GraphQL field errors][1] and are [not visible][9] in a [GraphQL schema][8] in any way.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Error payloads, `null`, and partial errors
|
||||||
|
|
||||||
|
[Juniper]'s error behavior conforms to the [GraphQL specification][0].
|
||||||
|
|
||||||
|
When a [field][6] returns an [error][11], the [field][6]'s result is replaced by `null`, and an additional `errors` object is created at the top level of the [response][7], and the [execution][5] is resumed.
|
||||||
|
|
||||||
|
Let's run the following query against the previous example:
|
||||||
|
```graphql
|
||||||
|
{
|
||||||
|
example {
|
||||||
|
contents
|
||||||
|
foo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If `str::from_utf8` results in a `std::str::Utf8Error`, then the following will be returned:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"example": {
|
||||||
|
"contents": "<Contents of the file>",
|
||||||
|
"foo": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": [{
|
||||||
|
"message": "invalid utf-8 sequence of 2 bytes from index 0",
|
||||||
|
"locations": [{"line": 2, "column": 4}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Since [`Non-Null` type][4] [fields][5] cannot be **null**, [field errors][1] are propagated to be handled by the parent [field][5]. If the parent [field][5] may be **null** then it resolves to **null**, otherwise if it is a [`Non-Null` type][4], the [field error][1] is further propagated to its parent [field][5].
|
||||||
|
|
||||||
|
For example, with the following query:
|
||||||
|
```graphql
|
||||||
|
{
|
||||||
|
example {
|
||||||
|
contents
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If the `File::open()` above results in a `std::io::ErrorKind::PermissionDenied`, the following ill be returned:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": null,
|
||||||
|
"errors": [{
|
||||||
|
"message": "Permission denied (os error 13)",
|
||||||
|
"locations": [{"line": 2, "column": 4}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Additional information
|
||||||
|
|
||||||
|
Sometimes it's desirable to return additional structured error information to clients. This can be accomplished by implementing the [`IntoFieldError` trait][23]:
|
||||||
|
```rust
|
||||||
|
# #[macro_use] extern crate juniper;
|
||||||
|
# use juniper::{graphql_object, FieldError, IntoFieldError, ScalarValue};
|
||||||
|
#
|
||||||
|
enum CustomError {
|
||||||
|
WhateverNotSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: ScalarValue> IntoFieldError<S> for CustomError {
|
||||||
|
fn into_field_error(self) -> FieldError<S> {
|
||||||
|
match self {
|
||||||
|
Self::WhateverNotSet => FieldError::new(
|
||||||
|
"Whatever does not exist",
|
||||||
|
graphql_value!({
|
||||||
|
"type": "NO_WHATEVER"
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Example {
|
||||||
|
whatever: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
impl Example {
|
||||||
|
fn whatever(&self) -> Result<bool, CustomError> {
|
||||||
|
if let Some(value) = self.whatever {
|
||||||
|
return Ok(value);
|
||||||
|
}
|
||||||
|
Err(CustomError::WhateverNotSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
And the specified structured error information will be included into the [error's `extensions`][2]:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"errors": [{
|
||||||
|
"message": "Whatever does not exist",
|
||||||
|
"locations": [{"line": 2, "column": 4}],
|
||||||
|
"extensions": {
|
||||||
|
"type": "NO_WHATEVER"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
> **NOTE**: This pattern is particularly useful when it comes to instrumentation of returned [field errors][1] with custom error codes or additional diagnostics (like stack traces or tracing IDs).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
[Rust]: https://www.rust-lang.org
|
||||||
|
|
||||||
|
[0]: https://spec.graphql.org/October2021#sec-Handling-Field-Errors
|
||||||
|
[1]: https://spec.graphql.org/October2021#sec-Errors.Field-errors
|
||||||
|
[2]: https://spec.graphql.org/October2021#sel-GAPHRPZCAACCC_7Q
|
||||||
|
[4]: https://spec.graphql.org/October2021#sec-Non-Null
|
||||||
|
[5]: https://spec.graphql.org/October2021#sec-Execution
|
||||||
|
[6]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||||
|
[7]: https://spec.graphql.org/October2021#sec-Response
|
||||||
|
[8]: https://graphql.org/learn/schema
|
||||||
|
[9]: https://spec.graphql.org/October2021#sec-Introspection
|
||||||
|
[11]: https://doc.rust-lang.org/book/ch09-00-error-handling.html
|
||||||
|
[12]: https://doc.rust-lang.org/stable/std/result/enum.Result.html
|
||||||
|
[13]: https://doc.rust-lang.org/stable/std/macro.panic.html
|
||||||
|
[14]: https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#a-shortcut-for-propagating-errors-the--operator
|
||||||
|
[15]: https://doc.rust-lang.org/stable/std/fmt/trait.Display.html
|
||||||
|
[21]: https://docs.rs/juniper/0.16.1/juniper/executor/type.FieldResult.html
|
||||||
|
[22]: https://docs.rs/juniper/0.16.1/juniper/executor/struct.FieldError.html
|
||||||
|
[23]: https://docs.rs/juniper/0.16.1/juniper/executor/trait.IntoFieldError.html
|
26
book/src/types/objects/error/index.md
Normal file
26
book/src/types/objects/error/index.md
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
Error handling
|
||||||
|
==============
|
||||||
|
|
||||||
|
Error handling in [GraphQL] can be done in multiple ways. We will cover the two different error handling models mostly used:
|
||||||
|
1. [Implicit field results](field.md).
|
||||||
|
2. [Explicit errors backend by GraphQL schema](schema.md).
|
||||||
|
|
||||||
|
Choosing the right error handling method depends on the requirements of the application and the concrete error happening. Investigating both approaches is beneficial.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Comparison
|
||||||
|
|
||||||
|
The [first approach](field.md) (where every error is a [field error][1]) is easier to implement. However, clients won't know what errors may occur and instead will have to infer what happens from the [error message][2]. This is brittle and could change over time due to either clients or server changing. Therefore, extensive integration testing between clients and server is required to maintain the implicit contract between the two.
|
||||||
|
|
||||||
|
[Encoding non-critical errors in a GraphQL schema](schema.md) makes the contract between clients and the server explicit. This allows clients to understand and handle these errors correctly and the server to know when changes are potentially breaking clients. However, encoding this error information into a [GraphQL schema][8] requires additional code and up-front definition of non-critical errors.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
|
||||||
|
[1]: https://spec.graphql.org/October2021#sec-Errors.Field-errors
|
||||||
|
[2]: https://spec.graphql.org/October2021/#sel-GAPHRPDCAACCyD57Z
|
||||||
|
[8]: https://graphql.org/learn/schema
|
336
book/src/types/objects/error/schema.md
Normal file
336
book/src/types/objects/error/schema.md
Normal file
|
@ -0,0 +1,336 @@
|
||||||
|
Schema errors
|
||||||
|
=============
|
||||||
|
|
||||||
|
[Rust]'s model of errors can be adapted for [GraphQL]. [Rust]'s panic is similar to a [field error][1] - the whole query is aborted and nothing can be extracted (except for error related information).
|
||||||
|
|
||||||
|
Not all errors require this strict handling. Recoverable or partial errors can be put into a [GraphQL schema][8], so the client can intelligently handle them.
|
||||||
|
|
||||||
|
To implement this approach, all errors must be partitioned into two classes:
|
||||||
|
- _Critical_ errors that cannot be fixed by clients (e.g. a database error).
|
||||||
|
- _Recoverable_ errors that can be fixed by clients (e.g. invalid input data).
|
||||||
|
|
||||||
|
Critical errors are returned from resolvers as [field errors][1] (from the [previous chapter](field.md)). Recoverable errors are part of a [GraphQL schema][8] and can be handled gracefully by clients. Similar to [Rust], [GraphQL] allows similar error models with [unions][9] (see ["Unions" chapter](../../unions.md)).
|
||||||
|
|
||||||
|
|
||||||
|
### Example: Simple
|
||||||
|
|
||||||
|
In this example, basic input validation is implemented with [GraphQL types][7]. [Strings][5] are used to identify the problematic [field][6] name. Errors for a particular [field][6] are also returned as a [string][5].
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::{graphql_object, GraphQLObject, GraphQLUnion};
|
||||||
|
#
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
pub struct Item {
|
||||||
|
name: String,
|
||||||
|
quantity: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
pub struct ValidationError {
|
||||||
|
field: String,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
pub struct ValidationErrors {
|
||||||
|
errors: Vec<ValidationError>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(GraphQLUnion)]
|
||||||
|
pub enum GraphQLResult {
|
||||||
|
Ok(Item),
|
||||||
|
Err(ValidationErrors),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Mutation;
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
impl Mutation {
|
||||||
|
fn add_item(&self, name: String, quantity: i32) -> GraphQLResult {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
if !(10 <= name.len() && name.len() <= 100) {
|
||||||
|
errors.push(ValidationError {
|
||||||
|
field: "name".into(),
|
||||||
|
message: "between 10 and 100".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(1 <= quantity && quantity <= 10) {
|
||||||
|
errors.push(ValidationError {
|
||||||
|
field: "quantity".into(),
|
||||||
|
message: "between 1 and 10".into(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
GraphQLResult::Ok(Item { name, quantity })
|
||||||
|
} else {
|
||||||
|
GraphQLResult::Err(ValidationErrors { errors })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each function may have a different return type and depending on the input parameters a new result type may be required. For example, adding a `User` would require a new result type containing the variant `Ok(User)`instead of `Ok(Item)`.
|
||||||
|
|
||||||
|
> **NOTE**: In this example the returned [string][5] contains a server-side localized error message. However, it is also
|
||||||
|
possible to return a unique string identifier and have the client present a localized string to its users.
|
||||||
|
|
||||||
|
The client can send a mutation request and handle the resulting errors in the following manner:
|
||||||
|
```graphql
|
||||||
|
{
|
||||||
|
mutation {
|
||||||
|
addItem(name: "", quantity: 0) {
|
||||||
|
... on Item {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
... on ValidationErrors {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **NOTE**: A useful side effect of this approach is to have partially successful queries or mutations. If one resolver fails, the results of the successful resolvers are not discarded.
|
||||||
|
|
||||||
|
|
||||||
|
### Example: Complex
|
||||||
|
|
||||||
|
Instead of using [strings][5] to propagate errors, it is possible to use [GraphQL type system][7] to describe the errors more precisely.
|
||||||
|
|
||||||
|
For each fallible [input argument][4] we create a [field][6] in a [GraphQL object][10]. The [field][6] is set if the validation for that particular [argument][4] fails.
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::{graphql_object, GraphQLObject, GraphQLUnion};
|
||||||
|
#
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
pub struct Item {
|
||||||
|
name: String,
|
||||||
|
quantity: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
pub struct ValidationError {
|
||||||
|
name: Option<String>,
|
||||||
|
quantity: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(GraphQLUnion)]
|
||||||
|
pub enum GraphQLResult {
|
||||||
|
Ok(Item),
|
||||||
|
Err(ValidationError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Mutation;
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
impl Mutation {
|
||||||
|
fn add_item(&self, name: String, quantity: i32) -> GraphQLResult {
|
||||||
|
let mut error = ValidationError {
|
||||||
|
name: None,
|
||||||
|
quantity: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !(10 <= name.len() && name.len() <= 100) {
|
||||||
|
error.name = Some("between 10 and 100".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(1 <= quantity && quantity <= 10) {
|
||||||
|
error.quantity = Some("between 1 and 10".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if error.name.is_none() && error.quantity.is_none() {
|
||||||
|
GraphQLResult::Ok(Item { name, quantity })
|
||||||
|
} else {
|
||||||
|
GraphQLResult::Err(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **NOTE**: We will likely want some kind of code generation to reduce repetition as the number of types required is significantly larger than before. Each resolver function has a custom `ValidationResult` which contains only [fields][6] provided by the function.
|
||||||
|
|
||||||
|
So, all the expected errors are handled directly inside the query. Additionally, all non-critical errors are known in advance by both the server and the client:
|
||||||
|
```graphql
|
||||||
|
{
|
||||||
|
mutation {
|
||||||
|
addItem {
|
||||||
|
... on Item {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
... on ValidationErrorsItem {
|
||||||
|
name
|
||||||
|
quantity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Example: Complex with critical errors
|
||||||
|
|
||||||
|
Our examples so far have only included non-critical errors. Providing errors inside a [GraphQL schema][8] still allows us to return unexpected critical errors when they occur.
|
||||||
|
|
||||||
|
In the following example, a theoretical database could fail and would generate errors. Since it is not common for a database to fail, the corresponding error is returned as a [critical error][1]:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::{graphql_object, graphql_value, FieldError, GraphQLObject, GraphQLUnion, ScalarValue};
|
||||||
|
#
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
pub struct Item {
|
||||||
|
name: String,
|
||||||
|
quantity: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
pub struct ValidationErrorItem {
|
||||||
|
name: Option<String>,
|
||||||
|
quantity: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(GraphQLUnion)]
|
||||||
|
pub enum GraphQLResult {
|
||||||
|
Ok(Item),
|
||||||
|
Err(ValidationErrorItem),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ApiError {
|
||||||
|
Database,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: ScalarValue> juniper::IntoFieldError<S> for ApiError {
|
||||||
|
fn into_field_error(self) -> FieldError<S> {
|
||||||
|
match self {
|
||||||
|
Self::Database => FieldError::new(
|
||||||
|
"Internal database error",
|
||||||
|
graphql_value!({"type": "DATABASE"}),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Mutation;
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
impl Mutation {
|
||||||
|
fn add_item(&self, name: String, quantity: i32) -> Result<GraphQLResult, ApiError> {
|
||||||
|
let mut error = ValidationErrorItem {
|
||||||
|
name: None,
|
||||||
|
quantity: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !(10 <= name.len() && name.len() <= 100) {
|
||||||
|
error.name = Some("between 10 and 100".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(1 <= quantity && quantity <= 10) {
|
||||||
|
error.quantity = Some("between 1 and 10".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if error.name.is_none() && error.quantity.is_none() {
|
||||||
|
Ok(GraphQLResult::Ok(Item { name, quantity }))
|
||||||
|
} else {
|
||||||
|
Ok(GraphQLResult::Err(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Example: Shopify API
|
||||||
|
|
||||||
|
The [Shopify API] implements a similar approach. Their API is a good reference to explore this approach in a real world application.
|
||||||
|
|
||||||
|
|
||||||
|
### Example: Non-struct [objects][10]
|
||||||
|
|
||||||
|
Up until now, we've only looked at mapping [structs][20] to [GraphQL objects][10]. However, any [Rust] type can be exposed a [GraphQL object][10].
|
||||||
|
|
||||||
|
Using `Result`-like [enums][1] can be a useful way of reporting validation errors from a mutation:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::{graphql_object, GraphQLObject};
|
||||||
|
#
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
struct User {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
struct ValidationError {
|
||||||
|
field: String,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SignUpResult {
|
||||||
|
Ok(User),
|
||||||
|
Error(Vec<ValidationError>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
impl SignUpResult {
|
||||||
|
fn user(&self) -> Option<&User> {
|
||||||
|
match self {
|
||||||
|
Self::Ok(user) => Some(user),
|
||||||
|
Self::Error(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error(&self) -> Option<&[ValidationError]> {
|
||||||
|
match self {
|
||||||
|
Self::Ok(_) => None,
|
||||||
|
Self::Error(errs) => Some(errs.as_slice())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, we use an [enum][21] to decide whether a client's input data is valid or not, and it could be used as the result of e.g. a `signUp` mutation:
|
||||||
|
```graphql
|
||||||
|
{
|
||||||
|
mutation {
|
||||||
|
signUp(name: "wrong") {
|
||||||
|
user {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
error {
|
||||||
|
field
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
[Rust]: https://www.rust-lang.org
|
||||||
|
[Shopify API]: https://shopify.dev/docs/admin-api/graphql/reference
|
||||||
|
|
||||||
|
[1]: https://spec.graphql.org/October2021#sec-Errors.Field-errors
|
||||||
|
[4]: https://spec.graphql.org/October2021#sec-Language.Arguments
|
||||||
|
[5]: https://spec.graphql.org/October2021#sec-String
|
||||||
|
[6]: https://spec.graphql.org/October2021#sec-Language.Fields
|
||||||
|
[7]: https://spec.graphql.org/October2021#sec-Types
|
||||||
|
[8]: https://graphql.org/learn/schema
|
||||||
|
[9]: https://spec.graphql.org/October2021#sec-Unions
|
||||||
|
[10]: https://spec.graphql.org/October2021#sec-Objects
|
||||||
|
[20]: https://doc.rust-lang.org/reference/items/structs.html
|
||||||
|
[21]: https://doc.rust-lang.org/reference/items/enumerations.html
|
|
@ -1,467 +0,0 @@
|
||||||
# Error handling
|
|
||||||
|
|
||||||
Error handling in GraphQL can be done in multiple ways. In the
|
|
||||||
following two different error handling models are discussed: field
|
|
||||||
results and GraphQL schema backed errors. Each approach has its
|
|
||||||
advantages. Choosing the right error handling method depends on the
|
|
||||||
requirements of the application--investigating both approaches is
|
|
||||||
beneficial.
|
|
||||||
|
|
||||||
## Field Results
|
|
||||||
|
|
||||||
Rust
|
|
||||||
[provides](https://doc.rust-lang.org/book/second-edition/ch09-00-error-handling.html)
|
|
||||||
two ways of dealing with errors: `Result<T, E>` for recoverable errors and
|
|
||||||
`panic!` for unrecoverable errors. Juniper does not do anything about panicking;
|
|
||||||
it will bubble up to the surrounding framework and hopefully be dealt with
|
|
||||||
there.
|
|
||||||
|
|
||||||
For recoverable errors, Juniper works well with the built-in `Result` type, you
|
|
||||||
can use the `?` operator and things will generally just work as you expect them to:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
use std::{
|
|
||||||
str,
|
|
||||||
path::PathBuf,
|
|
||||||
fs::{File},
|
|
||||||
io::{Read},
|
|
||||||
};
|
|
||||||
use juniper::{graphql_object, FieldResult};
|
|
||||||
|
|
||||||
struct Example {
|
|
||||||
filename: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[graphql_object]
|
|
||||||
impl Example {
|
|
||||||
fn contents(&self) -> FieldResult<String> {
|
|
||||||
let mut file = File::open(&self.filename)?;
|
|
||||||
let mut contents = String::new();
|
|
||||||
file.read_to_string(&mut contents)?;
|
|
||||||
Ok(contents)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn foo() -> FieldResult<Option<String>> {
|
|
||||||
// Some invalid bytes.
|
|
||||||
let invalid = vec![128, 223];
|
|
||||||
|
|
||||||
Ok(Some(str::from_utf8(&invalid)?.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
`FieldResult<T>` is an alias for `Result<T, FieldError>`, which is the error
|
|
||||||
type all fields must return. By using the `?` operator or `try!` macro, any type
|
|
||||||
that implements the `Display` trait - which are most of the error types out
|
|
||||||
there - those errors are automatically converted into `FieldError`.
|
|
||||||
|
|
||||||
## Error payloads, `null`, and partial errors
|
|
||||||
|
|
||||||
Juniper's error behavior conforms to the [GraphQL specification](https://spec.graphql.org/October2021#sec-Handling-Field-Errors).
|
|
||||||
|
|
||||||
When a field returns an error, the field's result is replaced by `null`, an
|
|
||||||
additional `errors` object is created at the top level of the response, and the
|
|
||||||
execution is resumed. For example, with the previous example and the following
|
|
||||||
query:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
{
|
|
||||||
example {
|
|
||||||
contents
|
|
||||||
foo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If `str::from_utf8` resulted in a `std::str::Utf8Error`, the following would be
|
|
||||||
returned:
|
|
||||||
|
|
||||||
!FILENAME Response for nullable field with error
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"example": {
|
|
||||||
contents: "<Contents of the file>",
|
|
||||||
foo: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"errors": [
|
|
||||||
"message": "invalid utf-8 sequence of 2 bytes from index 0",
|
|
||||||
"locations": [{ "line": 2, "column": 4 }])
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If an error is returned from a non-null field, such as the
|
|
||||||
example above, the `null` value is propagated up to the first nullable parent
|
|
||||||
field, or the root `data` object if there are no nullable fields.
|
|
||||||
|
|
||||||
For example, with the following query:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
{
|
|
||||||
example {
|
|
||||||
contents
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If `File::open()` above resulted in `std::io::ErrorKind::PermissionDenied`, the
|
|
||||||
following would be returned:
|
|
||||||
|
|
||||||
!FILENAME Response for non-null field with error and no nullable parent
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"errors": [
|
|
||||||
"message": "Permission denied (os error 13)",
|
|
||||||
"locations": [{ "line": 2, "column": 4 }])
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Structured errors
|
|
||||||
|
|
||||||
Sometimes it is desirable to return additional structured error information
|
|
||||||
to clients. This can be accomplished by implementing [`IntoFieldError`](https://docs.rs/juniper/latest/juniper/trait.IntoFieldError.html):
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# #[macro_use] extern crate juniper;
|
|
||||||
# use juniper::{graphql_object, FieldError, IntoFieldError, ScalarValue};
|
|
||||||
#
|
|
||||||
enum CustomError {
|
|
||||||
WhateverNotSet,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: ScalarValue> IntoFieldError<S> for CustomError {
|
|
||||||
fn into_field_error(self) -> FieldError<S> {
|
|
||||||
match self {
|
|
||||||
CustomError::WhateverNotSet => FieldError::new(
|
|
||||||
"Whatever does not exist",
|
|
||||||
graphql_value!({
|
|
||||||
"type": "NO_WHATEVER"
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Example {
|
|
||||||
whatever: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[graphql_object]
|
|
||||||
impl Example {
|
|
||||||
fn whatever(&self) -> Result<bool, CustomError> {
|
|
||||||
if let Some(value) = self.whatever {
|
|
||||||
return Ok(value);
|
|
||||||
}
|
|
||||||
Err(CustomError::WhateverNotSet)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
The specified structured error information is included in the [`extensions`](https://spec.graphql.org/October2021#sec-Errors) key:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"errors": [{
|
|
||||||
"message": "Whatever does not exist",
|
|
||||||
"locations": [{"line": 2, "column": 4}],
|
|
||||||
"extensions": {
|
|
||||||
"type": "NO_WHATEVER"
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Errors Backed by GraphQL's Schema
|
|
||||||
|
|
||||||
Rust's model of errors can be adapted for GraphQL. Rust's panic is
|
|
||||||
similar to a `FieldError`--the whole query is aborted and nothing can
|
|
||||||
be extracted (except for error related information).
|
|
||||||
|
|
||||||
Not all errors require this strict handling. Recoverable or partial errors can be put
|
|
||||||
into the GraphQL schema so the client can intelligently handle them.
|
|
||||||
|
|
||||||
To implement this approach, all errors must be partitioned into two error classes:
|
|
||||||
|
|
||||||
* Critical errors that cannot be fixed by the user (e.g. a database error).
|
|
||||||
* Recoverable errors that can be fixed by the user (e.g. invalid input data).
|
|
||||||
|
|
||||||
Critical errors are returned from resolvers as `FieldErrors` (from the previous section). Non-critical errors are part of the GraphQL schema and can be handled gracefully by clients. Similar to Rust, GraphQL allows similar error models with unions (see Unions).
|
|
||||||
|
|
||||||
### Example Input Validation (simple)
|
|
||||||
|
|
||||||
In this example, basic input validation is implemented with GraphQL
|
|
||||||
types. Strings are used to identify the problematic field name. Errors
|
|
||||||
for a particular field are also returned as a string. In this example
|
|
||||||
the string contains a server-side localized error message. However, it is also
|
|
||||||
possible to return a unique string identifier and have the client present a localized string to the user.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::{graphql_object, GraphQLObject, GraphQLUnion};
|
|
||||||
#
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
pub struct Item {
|
|
||||||
name: String,
|
|
||||||
quantity: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
pub struct ValidationError {
|
|
||||||
field: String,
|
|
||||||
message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
pub struct ValidationErrors {
|
|
||||||
errors: Vec<ValidationError>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLUnion)]
|
|
||||||
pub enum GraphQLResult {
|
|
||||||
Ok(Item),
|
|
||||||
Err(ValidationErrors),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Mutation;
|
|
||||||
|
|
||||||
#[graphql_object]
|
|
||||||
impl Mutation {
|
|
||||||
fn addItem(&self, name: String, quantity: i32) -> GraphQLResult {
|
|
||||||
let mut errors = Vec::new();
|
|
||||||
|
|
||||||
if !(10 <= name.len() && name.len() <= 100) {
|
|
||||||
errors.push(ValidationError {
|
|
||||||
field: "name".into(),
|
|
||||||
message: "between 10 and 100".into(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(1 <= quantity && quantity <= 10) {
|
|
||||||
errors.push(ValidationError {
|
|
||||||
field: "quantity".into(),
|
|
||||||
message: "between 1 and 10".into(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.is_empty() {
|
|
||||||
GraphQLResult::Ok(Item { name, quantity })
|
|
||||||
} else {
|
|
||||||
GraphQLResult::Err(ValidationErrors { errors })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
Each function may have a different return type and depending on the input
|
|
||||||
parameters a new result type is required. For example, adding a user
|
|
||||||
requires a new result type which contains the variant `Ok(User)`
|
|
||||||
instead of `Ok(Item)`.
|
|
||||||
|
|
||||||
The client can send a mutation request and handle the
|
|
||||||
resulting errors as shown in the following example:
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
{
|
|
||||||
mutation {
|
|
||||||
addItem(name: "", quantity: 0) {
|
|
||||||
... on Item {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
... on ValidationErrors {
|
|
||||||
errors {
|
|
||||||
field
|
|
||||||
message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
A useful side effect of this approach is to have partially successful
|
|
||||||
queries or mutations. If one resolver fails, the results of the
|
|
||||||
successful resolvers are not discarded.
|
|
||||||
|
|
||||||
### Example Input Validation (complex)
|
|
||||||
|
|
||||||
Instead of using strings to propagate errors, it is possible to use
|
|
||||||
GraphQL's type system to describe the errors more precisely.
|
|
||||||
|
|
||||||
For each fallible input variable a field in a GraphQL object is created. The
|
|
||||||
field is set if the validation for that particular field fails. You will likely want some kind of code generation to reduce repetition as the number of types required is significantly larger than
|
|
||||||
before. Each resolver function has a custom `ValidationResult` which
|
|
||||||
contains only fields provided by the function.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::{graphql_object, GraphQLObject, GraphQLUnion};
|
|
||||||
#
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
pub struct Item {
|
|
||||||
name: String,
|
|
||||||
quantity: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
pub struct ValidationError {
|
|
||||||
name: Option<String>,
|
|
||||||
quantity: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLUnion)]
|
|
||||||
pub enum GraphQLResult {
|
|
||||||
Ok(Item),
|
|
||||||
Err(ValidationError),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Mutation;
|
|
||||||
|
|
||||||
#[graphql_object]
|
|
||||||
impl Mutation {
|
|
||||||
fn addItem(&self, name: String, quantity: i32) -> GraphQLResult {
|
|
||||||
let mut error = ValidationError {
|
|
||||||
name: None,
|
|
||||||
quantity: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if !(10 <= name.len() && name.len() <= 100) {
|
|
||||||
error.name = Some("between 10 and 100".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(1 <= quantity && quantity <= 10) {
|
|
||||||
error.quantity = Some("between 1 and 10".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if error.name.is_none() && error.quantity.is_none() {
|
|
||||||
GraphQLResult::Ok(Item { name, quantity })
|
|
||||||
} else {
|
|
||||||
GraphQLResult::Err(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
```graphql
|
|
||||||
{
|
|
||||||
mutation {
|
|
||||||
addItem {
|
|
||||||
... on Item {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
... on ValidationErrorsItem {
|
|
||||||
name
|
|
||||||
quantity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected errors are handled directly inside the query. Additionally, all
|
|
||||||
non-critical errors are known in advance by both the server and the client.
|
|
||||||
|
|
||||||
### Example Input Validation (complex with critical error)
|
|
||||||
|
|
||||||
Our examples so far have only included non-critical errors. Providing
|
|
||||||
errors inside the GraphQL schema still allows you to return unexpected critical
|
|
||||||
errors when they occur.
|
|
||||||
|
|
||||||
In the following example, a theoretical database could fail
|
|
||||||
and would generate errors. Since it is not common for the database to
|
|
||||||
fail, the corresponding error is returned as a critical error:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
#
|
|
||||||
use juniper::{graphql_object, graphql_value, FieldError, GraphQLObject, GraphQLUnion, ScalarValue};
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
pub struct Item {
|
|
||||||
name: String,
|
|
||||||
quantity: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
pub struct ValidationErrorItem {
|
|
||||||
name: Option<String>,
|
|
||||||
quantity: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLUnion)]
|
|
||||||
pub enum GraphQLResult {
|
|
||||||
Ok(Item),
|
|
||||||
Err(ValidationErrorItem),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ApiError {
|
|
||||||
Database,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S: ScalarValue> juniper::IntoFieldError<S> for ApiError {
|
|
||||||
fn into_field_error(self) -> FieldError<S> {
|
|
||||||
match self {
|
|
||||||
ApiError::Database => FieldError::new(
|
|
||||||
"Internal database error",
|
|
||||||
graphql_value!({
|
|
||||||
"type": "DATABASE"
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Mutation;
|
|
||||||
|
|
||||||
#[graphql_object]
|
|
||||||
impl Mutation {
|
|
||||||
fn addItem(&self, name: String, quantity: i32) -> Result<GraphQLResult, ApiError> {
|
|
||||||
let mut error = ValidationErrorItem {
|
|
||||||
name: None,
|
|
||||||
quantity: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if !(10 <= name.len() && name.len() <= 100) {
|
|
||||||
error.name = Some("between 10 and 100".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(1 <= quantity && quantity <= 10) {
|
|
||||||
error.quantity = Some("between 1 and 10".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
if error.name.is_none() && error.quantity.is_none() {
|
|
||||||
Ok(GraphQLResult::Ok(Item { name, quantity }))
|
|
||||||
} else {
|
|
||||||
Ok(GraphQLResult::Err(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Additional Material
|
|
||||||
|
|
||||||
The [Shopify API](https://shopify.dev/docs/admin-api/graphql/reference)
|
|
||||||
implements a similar approach. Their API is a good reference to
|
|
||||||
explore this approach in a real world application.
|
|
||||||
|
|
||||||
# Comparison
|
|
||||||
|
|
||||||
The first approach discussed above--where every error is a critical error defined by `FieldResult` --is easier to implement. However, the client does not know what errors may occur and must instead infer what happened from the error string. This is brittle and could change over time due to either the client or server changing. Therefore, extensive integration testing between the client and server is required to maintain the implicit contract between the two.
|
|
||||||
|
|
||||||
Encoding non-critical errors in the GraphQL schema makes the contract between the client and the server explicit. This allows the client to understand and handle these errors correctly and the server to know when changes are potentially breaking clients. However, encoding this error information into the GraphQL schema requires additional code and up-front definition of non-critical errors.
|
|
78
book/src/types/objects/generics.md
Normal file
78
book/src/types/objects/generics.md
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
Generics
|
||||||
|
========
|
||||||
|
|
||||||
|
Yet another point where [GraphQL] and [Rust] differs is in how generics work:
|
||||||
|
- In [Rust], almost any type could be generic - that is, take type parameters.
|
||||||
|
- In [GraphQL], there are only two generic types: [lists][1] and [non-`null`ables][2].
|
||||||
|
|
||||||
|
This poses a restriction on what we can expose in [GraphQL] from [Rust]: no generic structs can be exposed - all type parameters must be bound. For example, we cannot expose `Result<T, E>` as a [GraphQL type][0], but we _can_ expose `Result<User, String>` as a [GraphQL type][0].
|
||||||
|
|
||||||
|
Let's make a slightly more compact but generic implementation of [the last schema error example](error/schema.md#example-non-struct-objects):
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::{graphql_object, GraphQLObject};
|
||||||
|
#
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
struct User {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
struct ForumPost {
|
||||||
|
title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
struct ValidationError {
|
||||||
|
field: String,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MutationResult<T>(Result<T, Vec<ValidationError>>);
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
#[graphql(name = "UserResult")]
|
||||||
|
impl MutationResult<User> {
|
||||||
|
fn user(&self) -> Option<&User> {
|
||||||
|
self.0.as_ref().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error(&self) -> Option<&[ValidationError]> {
|
||||||
|
self.0.as_ref().err().map(Vec::as_slice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
#[graphql(name = "ForumPostResult")]
|
||||||
|
impl MutationResult<ForumPost> {
|
||||||
|
fn forum_post(&self) -> Option<&ForumPost> {
|
||||||
|
self.0.as_ref().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error(&self) -> Option<&[ValidationError]> {
|
||||||
|
self.0.as_ref().err().map(Vec::as_slice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, we've made a wrapper around a `Result` and exposed some concrete instantiations of `Result<T, E>` as distinct [GraphQL objects][3].
|
||||||
|
|
||||||
|
> **NOTE**: The reason we needed the wrapper is of [Rust]'s [orphan rules][10] (both the `Result` and [Juniper]'s internal traits are from third-party sources).
|
||||||
|
|
||||||
|
> **NOTE**: Because we're using generics, we also need to specify a `name` for our instantiated [GraphQL types][0]. Even if [Juniper] _could_ figure out the name, `MutationResult<User>` wouldn't be a [valid GraphQL type name][4]. And, also, two different [GraphQL types][0] cannot have the same `MutationResult` name, inferred by default.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
[Rust]: https://www.rust-lang.org
|
||||||
|
|
||||||
|
[0]: https://spec.graphql.org/October2021#sec-Types
|
||||||
|
[1]: https://spec.graphql.org/October2021#sec-List
|
||||||
|
[2]: https://spec.graphql.org/October2021#sec-Non-Null
|
||||||
|
[3]: https://spec.graphql.org/October2021#sec-Objects
|
||||||
|
[4]: https://spec.graphql.org/October2021#sec-Names
|
||||||
|
[10]: https://doc.rust-lang.org/reference/items/implementations.html#trait-implementation-coherence
|
228
book/src/types/objects/index.md
Normal file
228
book/src/types/objects/index.md
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
Objects
|
||||||
|
=======
|
||||||
|
|
||||||
|
> [GraphQL objects][0] represent a list of named fields, each of which yield a value of a specific type.
|
||||||
|
|
||||||
|
When declaring a [GraphQL schema][schema], most of the time we deal with [GraphQL objects][0], because they are the only place where we actually define the behavior once [schema] gets [executed][1].
|
||||||
|
|
||||||
|
There are two ways to define a [GraphQL object][0] in [Juniper]:
|
||||||
|
1. The easiest way, suitable for trivial cases, is to use the [`#[derive(GraphQLObject)]` attribute][2] on a [struct], as described below.
|
||||||
|
2. The other way, using the [`#[graphql_object]` attribute][3], is described in the ["Complex fields" chapter](complex_fields.md).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Trivial
|
||||||
|
|
||||||
|
While any type in [Rust] can be exposed as a [GraphQL object][0], the most common one is a [struct]:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::GraphQLObject;
|
||||||
|
#
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
struct Person {
|
||||||
|
name: String,
|
||||||
|
age: i32,
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
This creates a [GraphQL object][0] type called `Person`, with two fields: `name` of type `String!`, and `age` of type `Int!`. Because of [Rust]'s type system, everything is exported as [non-`null`][4] by default.
|
||||||
|
|
||||||
|
> **TIP**: If a `null`able field is required, the most obvious way is to use `Option`. Or [`Nullable`] for distinguishing between [explicit and implicit `null`s][14].
|
||||||
|
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
We should take advantage of the fact that [GraphQL] is [self-documenting][5] and add descriptions to the defined [GraphQL object][0] type and its fields. [Juniper] will automatically use associated [Rust doc comments][6] as [GraphQL descriptions][7]:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::GraphQLObject;
|
||||||
|
#
|
||||||
|
/// Information about a person.
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
struct Person {
|
||||||
|
/// The person's full name, including both first and last names.
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
/// The person's age in years, rounded down.
|
||||||
|
age: i32,
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
If using [Rust doc comments][6] is not desired (for example, when we want to keep [Rust] API docs and GraphQL schema descriptions different), the `#[graphql(description = "...")]` attribute can be used instead, which takes precedence over [Rust doc comments][6]:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::GraphQLObject;
|
||||||
|
#
|
||||||
|
/// This doc comment is visible only in Rust API docs.
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
#[graphql(description = "This description is visible only in GraphQL schema.")]
|
||||||
|
struct Person {
|
||||||
|
/// This doc comment is visible only in Rust API docs.
|
||||||
|
#[graphql(desc = "This description is visible only in GraphQL schema.")]
|
||||||
|
// ^^^^ shortcut for a `description` argument
|
||||||
|
name: String,
|
||||||
|
|
||||||
|
/// This doc comment is visible in both Rust API docs and GraphQL schema
|
||||||
|
/// descriptions.
|
||||||
|
age: i32,
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Renaming
|
||||||
|
|
||||||
|
By default, [struct] fields are converted from [Rust]'s standard `snake_case` naming convention into [GraphQL]'s `camelCase` convention:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::GraphQLObject;
|
||||||
|
#
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
struct Person {
|
||||||
|
first_name: String, // exposed as `firstName` in GraphQL schema
|
||||||
|
last_name: String, // exposed as `lastName` in GraphQL schema
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
We can override the name by using the `#[graphql(name = "...")]` attribute:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::GraphQLObject;
|
||||||
|
#
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
#[graphql(name = "WebPerson")] // now exposed as `WebPerson` in GraphQL schema
|
||||||
|
struct Person {
|
||||||
|
name: String,
|
||||||
|
age: i32,
|
||||||
|
#[graphql(name = "websiteURL")]
|
||||||
|
website_url: Option<String>, // now exposed as `websiteURL` in GraphQL schema
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or provide a different renaming policy for all the [struct] fields:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::GraphQLObject;
|
||||||
|
#
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
#[graphql(rename_all = "none")] // disables any renaming
|
||||||
|
struct Person {
|
||||||
|
name: String,
|
||||||
|
age: i32,
|
||||||
|
website_url: Option<String>, // exposed as `website_url` in GraphQL schema
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
> **TIP**: Supported policies are: `SCREAMING_SNAKE_CASE`, `camelCase` and `none` (disables any renaming).
|
||||||
|
|
||||||
|
|
||||||
|
### Deprecation
|
||||||
|
|
||||||
|
To [deprecate][9] a [GraphQL object][0] field, either the `#[graphql(deprecated = "...")]` attribute, or [Rust's `#[deprecated]` attribute][13], should be used:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::GraphQLObject;
|
||||||
|
#
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
struct Person {
|
||||||
|
name: String,
|
||||||
|
age: i32,
|
||||||
|
#[graphql(deprecated = "Please use the `name` field instead.")]
|
||||||
|
first_name: String,
|
||||||
|
#[deprecated(note = "Please use the `name` field instead.")]
|
||||||
|
last_name: String,
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
> **NOTE**: Only [GraphQL object][0]/[interface][11] fields and [GraphQL enum][10] values can be [deprecated][9].
|
||||||
|
|
||||||
|
|
||||||
|
### Ignoring
|
||||||
|
|
||||||
|
By default, all [struct] fields are included into the generated [GraphQL object][0] type. To prevent inclusion of a specific field annotate it with the `#[graphql(ignore)]` attribute:
|
||||||
|
```rust
|
||||||
|
# #![allow(dead_code)]
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::GraphQLObject;
|
||||||
|
#
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
struct Person {
|
||||||
|
name: String,
|
||||||
|
age: i32,
|
||||||
|
#[graphql(ignore)]
|
||||||
|
password_hash: String, // cannot be queried from GraphQL
|
||||||
|
#[graphql(skip)]
|
||||||
|
// ^^^^ alternative naming, up to your preference
|
||||||
|
is_banned: bool, // cannot be queried from GraphQL
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **TIP**: See more available features in the API docs of the [`#[derive(GraphQLObject)]`][2] attribute.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Relationships
|
||||||
|
|
||||||
|
[GraphQL object][0] fields can be of any [GraphQL] type, except [input objects][8].
|
||||||
|
|
||||||
|
Let's see what it means to build relationships between [objects][0]:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::GraphQLObject;
|
||||||
|
#
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
struct Person {
|
||||||
|
name: String,
|
||||||
|
age: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(GraphQLObject)]
|
||||||
|
struct House {
|
||||||
|
address: Option<String>, // converted into `String` (`null`able)
|
||||||
|
inhabitants: Vec<Person>, // converted into `[Person!]!`
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Because `Person` is a valid [GraphQL] type, we can have a `Vec<Person>` in a [struct], and it'll be automatically converted into a [list][12] of [non-`null`able][4] `Person` [objects][0].
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[`Nullable`]: https://docs.rs/juniper/0.16.1/juniper/enum.Nullable.html
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
|
[Rust]: https://www.rust-lang.org
|
||||||
|
[schema]: https://graphql.org/learn/schema
|
||||||
|
[struct]: https://doc.rust-lang.org/reference/items/structs.html
|
||||||
|
|
||||||
|
[0]: https://spec.graphql.org/October2021#sec-Objects
|
||||||
|
[1]: https://spec.graphql.org/October2021#sec-Execution
|
||||||
|
[2]: https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLObject.html
|
||||||
|
[3]: https://docs.rs/juniper/0.16.1/juniper/attr.graphql_object.html
|
||||||
|
[4]: https://spec.graphql.org/October2021#sec-Non-Null
|
||||||
|
[5]: https://spec.graphql.org/October2021#sec-Introspection
|
||||||
|
[6]: https://doc.rust-lang.org/reference/comments.html#doc-comments
|
||||||
|
[7]: https://spec.graphql.org/October2021#sec-Descriptions
|
||||||
|
[8]: https://spec.graphql.org/October2021#sec-Input-Objects
|
||||||
|
[9]: https://spec.graphql.org/October2021#sec--deprecated
|
||||||
|
[10]: https://spec.graphql.org/October2021#sec-Enums
|
||||||
|
[11]: https://spec.graphql.org/October2021#sec-Interfaces
|
||||||
|
[12]: https://spec.graphql.org/October2021#sec-List
|
||||||
|
[13]: https://doc.rust-lang.org/reference/attributes/diagnostics.html#the-deprecated-attribute
|
||||||
|
[14]: https://spec.graphql.org/October2021#sel-EAFdRDHAAEJDAoBxzT
|
|
@ -1,151 +0,0 @@
|
||||||
# Using contexts
|
|
||||||
|
|
||||||
The context type is a feature in Juniper that lets field resolvers access global
|
|
||||||
data, most commonly database connections or authentication information. The
|
|
||||||
context is usually created from a _context factory_. How this is defined is
|
|
||||||
specific to the framework integration you're using, so check out the
|
|
||||||
documentation for either the [Iron](../../servers/iron.md) or [Rocket](../../servers/rocket.md)
|
|
||||||
integration.
|
|
||||||
|
|
||||||
In this chapter, we'll show you how to define a context type and use it in field
|
|
||||||
resolvers. Let's say that we have a simple user database in a `HashMap`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# #![allow(dead_code)]
|
|
||||||
# use std::collections::HashMap;
|
|
||||||
#
|
|
||||||
struct Database {
|
|
||||||
users: HashMap<i32, User>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct User {
|
|
||||||
id: i32,
|
|
||||||
name: String,
|
|
||||||
friend_ids: Vec<i32>,
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() { }
|
|
||||||
```
|
|
||||||
|
|
||||||
We would like a `friends` field on `User` that returns a list of `User` objects.
|
|
||||||
In order to write such a field though, the database must be queried.
|
|
||||||
|
|
||||||
To solve this, we mark the `Database` as a valid context type and assign it to
|
|
||||||
the user object.
|
|
||||||
|
|
||||||
To gain access to the context, we need to specify an argument with the same
|
|
||||||
type as the specified `Context` for the type:
|
|
||||||
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use std::collections::HashMap;
|
|
||||||
# use juniper::graphql_object;
|
|
||||||
#
|
|
||||||
// This struct represents our context.
|
|
||||||
struct Database {
|
|
||||||
users: HashMap<i32, User>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark the Database as a valid context type for Juniper
|
|
||||||
impl juniper::Context for Database {}
|
|
||||||
|
|
||||||
struct User {
|
|
||||||
id: i32,
|
|
||||||
name: String,
|
|
||||||
friend_ids: Vec<i32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign Database as the context type for User
|
|
||||||
#[graphql_object(context = Database)]
|
|
||||||
impl User {
|
|
||||||
// Inject the context by specifying an argument with the context type.
|
|
||||||
// Note:
|
|
||||||
// - the type must be a reference
|
|
||||||
// - the name of the argument SHOULD be `context`
|
|
||||||
fn friends<'db>(&self, context: &'db Database) -> Vec<&'db User> {
|
|
||||||
// Use the database to lookup users
|
|
||||||
self.friend_ids.iter()
|
|
||||||
.map(|id| context.users.get(id).expect("Could not find user with ID"))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
self.name.as_str()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> i32 {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() { }
|
|
||||||
```
|
|
||||||
|
|
||||||
You only get an immutable reference to the context, so if you want to affect
|
|
||||||
change to the execution, you'll need to use [interior
|
|
||||||
mutability](https://doc.rust-lang.org/book/first-edition/mutability.html#interior-vs-exterior-mutability)
|
|
||||||
using e.g. `RwLock` or `RefCell`.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Dealing with mutable references
|
|
||||||
|
|
||||||
Context cannot be specified by a mutable reference, because concurrent fields resolving may be performed. If you have something in your context that requires access by mutable reference, then you need to leverage the [interior mutability][1] for that.
|
|
||||||
|
|
||||||
For example, when using async runtime with [work stealing][2] (like `tokio`), which obviously requires thread safety in addition, you will need to use a corresponding async version of `RwLock`:
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# extern crate tokio;
|
|
||||||
# use std::collections::HashMap;
|
|
||||||
# use juniper::graphql_object;
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
|
|
||||||
struct Database {
|
|
||||||
requested_count: HashMap<i32, i32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since we cannot directly implement juniper::Context
|
|
||||||
// for RwLock we use the newtype idiom
|
|
||||||
struct DatabaseContext(RwLock<Database>);
|
|
||||||
|
|
||||||
impl juniper::Context for DatabaseContext {}
|
|
||||||
|
|
||||||
struct User {
|
|
||||||
id: i32,
|
|
||||||
name: String
|
|
||||||
}
|
|
||||||
|
|
||||||
#[graphql_object(context=DatabaseContext)]
|
|
||||||
impl User {
|
|
||||||
async fn times_requested<'db>(&self, context: &'db DatabaseContext) -> i32 {
|
|
||||||
// Acquire a mutable reference and await if async RwLock is used,
|
|
||||||
// which is necessary if context consists async operations like
|
|
||||||
// querying remote databases.
|
|
||||||
// Obtain base type
|
|
||||||
let DatabaseContext(context) = context;
|
|
||||||
// If context is immutable use .read() on RwLock.
|
|
||||||
let mut context = context.write().await;
|
|
||||||
// Preform a mutable operation.
|
|
||||||
context.requested_count.entry(self.id).and_modify(|e| { *e += 1 }).or_insert(1).clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
self.name.as_str()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> i32 {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() { }
|
|
||||||
```
|
|
||||||
Replace `tokio::sync::RwLock` with `std::sync::RwLock` (or similar) if you don't intend to use async resolving.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[1]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html
|
|
||||||
[2]: https://en.wikipedia.org/wiki/Work_stealing
|
|
|
@ -1,11 +0,0 @@
|
||||||
# Other Types
|
|
||||||
|
|
||||||
The GraphQL type system provides several types in additon to objects.
|
|
||||||
|
|
||||||
Find out more about each type below:
|
|
||||||
|
|
||||||
- [Enums](enums.md)
|
|
||||||
- [Interfaces](interfaces.md)
|
|
||||||
- [Input objects](input_objects.md)
|
|
||||||
- [Scalars](scalars.md)
|
|
||||||
- [Unions](unions.md)
|
|
|
@ -1,118 +1,88 @@
|
||||||
# Scalars
|
Scalars
|
||||||
|
=======
|
||||||
|
|
||||||
Scalars are the primitive types at the leaves of a GraphQL query: numbers,
|
[GraphQL scalars][0] represent primitive leaf values in a GraphQL type system: numbers, strings, and booleans.
|
||||||
strings, and booleans. You can create custom scalars to other primitive values,
|
|
||||||
but this often requires coordination with the client library intended to consume
|
|
||||||
the API you're building.
|
|
||||||
|
|
||||||
Since any value going over the wire is eventually transformed into JSON, you're
|
|
||||||
also limited in the data types you can use.
|
|
||||||
|
|
||||||
There are two ways to define custom scalars.
|
|
||||||
* For simple scalars that just wrap a primitive type, you can use the newtype pattern with
|
|
||||||
a custom derive.
|
|
||||||
* For more advanced use cases with custom validation, you can use
|
|
||||||
the `graphql_scalar` proc macro.
|
|
||||||
|
|
||||||
|
|
||||||
## Built-in scalars
|
|
||||||
|
|
||||||
Juniper has built-in support for:
|
|
||||||
|
|
||||||
* `i32` as `Int`
|
|
||||||
* `f64` as `Float`
|
|
||||||
* `String` and `&str` as `String`
|
|
||||||
* `bool` as `Boolean`
|
|
||||||
* `juniper::ID` as `ID`. This type is defined [in the
|
|
||||||
spec](https://spec.graphql.org/October2021#sec-ID) as a type that is serialized
|
|
||||||
as a string but can be parsed from both a string and an integer.
|
|
||||||
|
|
||||||
Note that there is no built-in support for `i64`/`u64`, as the GraphQL spec [doesn't define any built-in scalars for `i64`/`u64` by default](https://spec.graphql.org/October2021#sec-Int). You may wish to leverage a [custom GraphQL scalar](#custom-scalars) in your schema to support them.
|
|
||||||
|
|
||||||
**Third party types**:
|
|
||||||
|
|
||||||
Juniper has built-in support for a few additional types from common third party
|
|
||||||
crates. They are enabled via features that are on by default.
|
|
||||||
|
|
||||||
* uuid::Uuid
|
|
||||||
* chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime}
|
|
||||||
* chrono_tz::Tz;
|
|
||||||
* time::{Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset}
|
|
||||||
* url::Url
|
|
||||||
* bson::oid::ObjectId
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Custom scalars
|
## Built-in
|
||||||
|
|
||||||
### `#[graphql(transparent)]` attribute
|
[Juniper] provides support for all the [built-in scalars][5].
|
||||||
|
|
||||||
Often, you might need a custom scalar that just wraps an existing type.
|
| [Rust] types | [GraphQL] scalar |
|
||||||
|
|------------------|------------------|
|
||||||
|
| `bool` | `Boolean` |
|
||||||
|
| `i32` | `Int` |
|
||||||
|
| `f64` | `Float` |
|
||||||
|
| `String`, `&str` | `String` |
|
||||||
|
| `juniper::ID` | [`ID`] |
|
||||||
|
|
||||||
This can be done with the newtype pattern and a custom derive, similar to how
|
> **NOTE**: [`ID`] scalar is [defined in the GraphQL spec][`ID`] as a type that is serialized as a string, but can be parsed from both a string and an integer.
|
||||||
serde supports this pattern with `#[serde(transparent)]`.
|
|
||||||
|
|
||||||
|
> **TIP**: There is no built-in support for `i64`, `u64`, or other [Rust] integer types, as the [GraphQL spec doesn't define any built-in scalars for them][1] by default. Instead, to be supported, they should be defined as [custom scalars](#custom) in a [GraphQL schema][schema].
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Custom
|
||||||
|
|
||||||
|
We can create [custom scalars][2] for other primitive values, but they are still [limited in the data types for representation][1], and only introduce additional semantic meaning. This, also, often requires coordination with the client library, intended to consume the API we're building.
|
||||||
|
|
||||||
|
[Custom scalars][2] can be defined in [Juniper] by using either [`#[derive(GraphQLScalar)]`][8] or [`#[graphql_scalar]`][9] attributes, which do work pretty much the same way (except, [`#[derive(GraphQLScalar)]`][8] cannot be used on [type aliases][4]).
|
||||||
|
|
||||||
|
|
||||||
|
### Transparent delegation
|
||||||
|
|
||||||
|
Quite often, we want to create a [custom GraphQL scalar][2] type by just wrapping an existing one, inheriting all its behavior. In [Rust], this is often called as ["newtype pattern"][3]. This may be achieved by providing a `#[graphql(transparent)]` attribute to the definition:
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
|
# use juniper::{graphql_scalar, GraphQLScalar};
|
||||||
#
|
#
|
||||||
#[derive(juniper::GraphQLScalar)]
|
#[derive(GraphQLScalar)]
|
||||||
#[graphql(transparent)]
|
#[graphql(transparent)]
|
||||||
pub struct UserId(i32);
|
pub struct UserId(i32);
|
||||||
|
|
||||||
#[derive(juniper::GraphQLObject)]
|
// Using `#[graphql_scalar]` attribute here makes no difference, and is fully
|
||||||
struct User {
|
// interchangeable with `#[derive(GraphQLScalar)]`. It's only up to the
|
||||||
id: UserId,
|
// personal preference - which one to use.
|
||||||
}
|
#[graphql_scalar]
|
||||||
#
|
#[graphql(transparent)]
|
||||||
# fn main() {}
|
pub struct MessageId {
|
||||||
```
|
|
||||||
|
|
||||||
`#[derive(GraphQLScalar)]` is mostly interchangeable with `#[graphql_scalar]`
|
|
||||||
attribute:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::graphql_scalar;
|
|
||||||
#
|
|
||||||
#[graphql_scalar(transparent)]
|
|
||||||
pub struct UserId {
|
|
||||||
value: i32,
|
value: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(juniper::GraphQLObject)]
|
|
||||||
struct User {
|
|
||||||
id: UserId,
|
|
||||||
}
|
|
||||||
#
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
That's it, now the `UserId` and `MessageId` [scalars][0] can be used in [GraphQL schema][schema].
|
||||||
|
|
||||||
That's it, you can now use `UserId` in your schema.
|
We may also customize the definition, to provide more information about our [custom scalar][2] in [GraphQL schema][schema]:
|
||||||
|
|
||||||
The macro also allows for more customization:
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
/// You can use a doc comment to specify a description.
|
# use juniper::GraphQLScalar;
|
||||||
#[derive(juniper::GraphQLScalar)]
|
#
|
||||||
|
/// You can use a Rust doc comment to specify a description in GraphQL schema.
|
||||||
|
#[derive(GraphQLScalar)]
|
||||||
#[graphql(
|
#[graphql(
|
||||||
transparent,
|
transparent,
|
||||||
// Overwrite the GraphQL type name.
|
// Overwrite the name of this type in the GraphQL schema.
|
||||||
name = "MyUserId",
|
name = "MyUserId",
|
||||||
// Specify a custom description.
|
// Specifying a type description via attribute takes precedence over the
|
||||||
// A description in the attribute will overwrite a doc comment.
|
// Rust doc comment, which allows to separate Rust API docs from GraphQL
|
||||||
description = "My user id description",
|
// schema descriptions, if required.
|
||||||
|
description = "Actual description.",
|
||||||
|
// Optional specification URL.
|
||||||
|
specified_by_url = "https://tools.ietf.org/html/rfc4122",
|
||||||
)]
|
)]
|
||||||
pub struct UserId(i32);
|
pub struct UserId(String);
|
||||||
#
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
All the methods used from newtype's field can be replaced with attributes:
|
|
||||||
|
|
||||||
### `#[graphql(to_output_with = <fn>)]` attribute
|
### Resolving
|
||||||
|
|
||||||
|
In case we need to customize [resolving][7] of a [custom GraphQL scalar][2] value (change the way it gets executed), the `#[graphql(to_output_with = <fn path>)]` attribute is the way to do so:
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
# use juniper::{GraphQLScalar, ScalarValue, Value};
|
# use juniper::{GraphQLScalar, ScalarValue, Value};
|
||||||
|
@ -123,14 +93,17 @@ struct Incremented(i32);
|
||||||
|
|
||||||
/// Increments [`Incremented`] before converting into a [`Value`].
|
/// Increments [`Incremented`] before converting into a [`Value`].
|
||||||
fn to_output<S: ScalarValue>(v: &Incremented) -> Value<S> {
|
fn to_output<S: ScalarValue>(v: &Incremented) -> Value<S> {
|
||||||
Value::from(v.0 + 1)
|
let inc = v.0 + 1;
|
||||||
|
Value::from(inc)
|
||||||
}
|
}
|
||||||
#
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `#[graphql(from_input_with = <fn>)]` attribute
|
|
||||||
|
|
||||||
|
### Input value parsing
|
||||||
|
|
||||||
|
Customization of a [custom GraphQL scalar][2] value parsing is possible via `#[graphql(from_input_with = <fn path>)]` attribute:
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
# use juniper::{GraphQLScalar, InputValue, ScalarValue};
|
# use juniper::{GraphQLScalar, InputValue, ScalarValue};
|
||||||
|
@ -140,8 +113,8 @@ fn to_output<S: ScalarValue>(v: &Incremented) -> Value<S> {
|
||||||
struct UserId(String);
|
struct UserId(String);
|
||||||
|
|
||||||
impl UserId {
|
impl UserId {
|
||||||
/// Checks whether [`InputValue`] is `String` beginning with `id: ` and
|
/// Checks whether the [`InputValue`] is a [`String`] beginning with `id: `
|
||||||
/// strips it.
|
/// and strips it.
|
||||||
fn from_input<S>(input: &InputValue<S>) -> Result<Self, String>
|
fn from_input<S>(input: &InputValue<S>) -> Result<Self, String>
|
||||||
where
|
where
|
||||||
S: ScalarValue
|
S: ScalarValue
|
||||||
|
@ -164,13 +137,15 @@ impl UserId {
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `#[graphql(parse_token_with = <fn>]` or `#[graphql(parse_token(<types>)]` attributes
|
|
||||||
|
|
||||||
|
### Token parsing
|
||||||
|
|
||||||
|
Customization of which tokens a [custom GraphQL scalar][0] type should be parsed from, is possible via `#[graphql(parse_token_with = <fn path>)]` or `#[graphql(parse_token(<types>)]` attributes:
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
# use juniper::{
|
# use juniper::{
|
||||||
# GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
|
# GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
|
||||||
# ScalarValue, ScalarToken, Value
|
# ScalarValue, ScalarToken, Value,
|
||||||
# };
|
# };
|
||||||
#
|
#
|
||||||
#[derive(GraphQLScalar)]
|
#[derive(GraphQLScalar)]
|
||||||
|
@ -178,32 +153,26 @@ impl UserId {
|
||||||
to_output_with = to_output,
|
to_output_with = to_output,
|
||||||
from_input_with = from_input,
|
from_input_with = from_input,
|
||||||
parse_token_with = parse_token,
|
parse_token_with = parse_token,
|
||||||
// ^^^^^^^^^^^^^^^^ Can be replaced with `parse_token(String, i32)`
|
|
||||||
// which tries to parse as `String` and then as `i32`
|
|
||||||
// if prior fails.
|
|
||||||
)]
|
)]
|
||||||
|
// ^^^^^^^^^^^^^^^^ Can be replaced with `parse_token(String, i32)`, which
|
||||||
|
// tries to parse as `String` first, and then as `i32` if
|
||||||
|
// prior fails.
|
||||||
enum StringOrInt {
|
enum StringOrInt {
|
||||||
String(String),
|
String(String),
|
||||||
Int(i32),
|
Int(i32),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_output<S>(v: &StringOrInt) -> Value<S>
|
fn to_output<S: ScalarValue>(v: &StringOrInt) -> Value<S> {
|
||||||
where
|
|
||||||
S: ScalarValue
|
|
||||||
{
|
|
||||||
match v {
|
match v {
|
||||||
StringOrInt::String(s) => Value::scalar(s.to_owned()),
|
StringOrInt::String(s) => Value::scalar(s.to_owned()),
|
||||||
StringOrInt::Int(i) => Value::scalar(*i),
|
StringOrInt::Int(i) => Value::scalar(*i),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_input<S>(v: &InputValue<S>) -> Result<StringOrInt, String>
|
fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<StringOrInt, String> {
|
||||||
where
|
|
||||||
S: ScalarValue
|
|
||||||
{
|
|
||||||
v.as_string_value()
|
v.as_string_value()
|
||||||
.map(|s| StringOrInt::String(s.into()))
|
.map(|s| StringOrInt::String(s.into()))
|
||||||
.or_else(|| v.as_int_value().map(|i| StringOrInt::Int(i)))
|
.or_else(|| v.as_int_value().map(StringOrInt::Int))
|
||||||
.ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
|
.ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,27 +183,62 @@ fn parse_token<S: ScalarValue>(value: ScalarToken<'_>) -> ParseScalarResult<S> {
|
||||||
#
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
> **NOTE**: Once we provide all 3 custom functions, there is no sense to follow [newtype pattern][3] anymore, as nothing left to inherit.
|
||||||
|
|
||||||
> __NOTE:__ As you can see, once you provide all 3 custom resolvers, there
|
|
||||||
> is no need to follow `newtype` pattern.
|
|
||||||
|
|
||||||
### `#[graphql(with = <path>)]` attribute
|
### Full behavior
|
||||||
|
|
||||||
Instead of providing all custom resolvers, you can provide path to the `to_output`,
|
|
||||||
`from_input`, `parse_token` functions.
|
|
||||||
|
|
||||||
Path can be simply `with = Self` (default path where macro expects resolvers to be),
|
|
||||||
in case there is an impl block with custom resolvers:
|
|
||||||
|
|
||||||
|
Instead of providing all custom functions separately, it's possible to provide a module holding the appropriate `to_output()`, `from_input()` and `parse_token()` functions via `#[graphql(with = <module path>)]` attribute:
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
# use juniper::{
|
# use juniper::{
|
||||||
# GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
|
# GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
|
||||||
# ScalarValue, ScalarToken, Value
|
# ScalarValue, ScalarToken, Value,
|
||||||
# };
|
# };
|
||||||
#
|
#
|
||||||
#[derive(GraphQLScalar)]
|
#[derive(GraphQLScalar)]
|
||||||
// #[graphql(with = Self)] <- default behaviour
|
#[graphql(with = string_or_int)]
|
||||||
|
enum StringOrInt {
|
||||||
|
String(String),
|
||||||
|
Int(i32),
|
||||||
|
}
|
||||||
|
|
||||||
|
mod string_or_int {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub(super) fn to_output<S: ScalarValue>(v: &StringOrInt) -> Value<S> {
|
||||||
|
match v {
|
||||||
|
StringOrInt::String(s) => Value::scalar(s.to_owned()),
|
||||||
|
StringOrInt::Int(i) => Value::scalar(*i),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<StringOrInt, String> {
|
||||||
|
v.as_string_value()
|
||||||
|
.map(|s| StringOrInt::String(s.into()))
|
||||||
|
.or_else(|| v.as_int_value().map(StringOrInt::Int))
|
||||||
|
.ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_token<S: ScalarValue>(t: ScalarToken<'_>) -> ParseScalarResult<S> {
|
||||||
|
<String as ParseScalarValue<S>>::from_str(t)
|
||||||
|
.or_else(|_| <i32 as ParseScalarValue<S>>::from_str(t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
A regular `impl` block is also suitable for that:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::{
|
||||||
|
# GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
|
||||||
|
# ScalarValue, ScalarToken, Value,
|
||||||
|
# };
|
||||||
|
#
|
||||||
|
#[derive(GraphQLScalar)]
|
||||||
|
// #[graphql(with = Self)] <- default behaviour, so can be omitted
|
||||||
enum StringOrInt {
|
enum StringOrInt {
|
||||||
String(String),
|
String(String),
|
||||||
Int(i32),
|
Int(i32),
|
||||||
|
@ -247,20 +251,20 @@ impl StringOrInt {
|
||||||
Self::Int(i) => Value::scalar(*i),
|
Self::Int(i) => Value::scalar(*i),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_input<S>(v: &InputValue<S>) -> Result<Self, String>
|
fn from_input<S>(v: &InputValue<S>) -> Result<Self, String>
|
||||||
where
|
where
|
||||||
S: ScalarValue,
|
S: ScalarValue
|
||||||
{
|
{
|
||||||
v.as_string_value()
|
v.as_string_value()
|
||||||
.map(|s| Self::String(s.into()))
|
.map(|s| Self::String(s.into()))
|
||||||
.or_else(|| v.as_int_value().map(Self::Int))
|
.or_else(|| v.as_int_value().map(Self::Int))
|
||||||
.ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
|
.ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_token<S>(value: ScalarToken<'_>) -> ParseScalarResult<S>
|
fn parse_token<S>(value: ScalarToken<'_>) -> ParseScalarResult<S>
|
||||||
where
|
where
|
||||||
S: ScalarValue,
|
S: ScalarValue
|
||||||
{
|
{
|
||||||
<String as ParseScalarValue<S>>::from_str(value)
|
<String as ParseScalarValue<S>>::from_str(value)
|
||||||
.or_else(|_| <i32 as ParseScalarValue<S>>::from_str(value))
|
.or_else(|_| <i32 as ParseScalarValue<S>>::from_str(value))
|
||||||
|
@ -270,17 +274,19 @@ impl StringOrInt {
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
Or it can be path to a module, where custom resolvers are located.
|
At the same time, any custom function still may be specified separately, if required:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
# use juniper::{
|
# use juniper::{
|
||||||
# GraphQLScalar, InputValue, ParseScalarResult, ParseScalarValue,
|
# GraphQLScalar, InputValue, ParseScalarResult, ScalarValue,
|
||||||
# ScalarValue, ScalarToken, Value
|
# ScalarToken, Value
|
||||||
# };
|
# };
|
||||||
#
|
#
|
||||||
#[derive(GraphQLScalar)]
|
#[derive(GraphQLScalar)]
|
||||||
#[graphql(with = string_or_int)]
|
#[graphql(
|
||||||
|
with = string_or_int,
|
||||||
|
parse_token(String, i32)
|
||||||
|
)]
|
||||||
enum StringOrInt {
|
enum StringOrInt {
|
||||||
String(String),
|
String(String),
|
||||||
Int(i32),
|
Int(i32),
|
||||||
|
@ -298,7 +304,7 @@ mod string_or_int {
|
||||||
StringOrInt::Int(i) => Value::scalar(*i),
|
StringOrInt::Int(i) => Value::scalar(*i),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn from_input<S>(v: &InputValue<S>) -> Result<StringOrInt, String>
|
pub(super) fn from_input<S>(v: &InputValue<S>) -> Result<StringOrInt, String>
|
||||||
where
|
where
|
||||||
S: ScalarValue,
|
S: ScalarValue,
|
||||||
|
@ -308,62 +314,23 @@ mod string_or_int {
|
||||||
.or_else(|| v.as_int_value().map(StringOrInt::Int))
|
.or_else(|| v.as_int_value().map(StringOrInt::Int))
|
||||||
.ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
|
.ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn parse_token<S>(value: ScalarToken<'_>) -> ParseScalarResult<S>
|
// No need in `parse_token()` function.
|
||||||
where
|
|
||||||
S: ScalarValue,
|
|
||||||
{
|
|
||||||
<String as ParseScalarValue<S>>::from_str(value)
|
|
||||||
.or_else(|_| <i32 as ParseScalarValue<S>>::from_str(value))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
Also, you can partially override `#[graphql(with)]` attribute with other custom scalars.
|
> **TIP**: See more available features in the API docs of the [`#[derive(GraphQLScalar)]`][8] and [`#[graphql_scalar]`][9] attributes.
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use juniper::{GraphQLScalar, InputValue, ParseScalarResult, ScalarValue, ScalarToken, Value};
|
|
||||||
#
|
|
||||||
#[derive(GraphQLScalar)]
|
|
||||||
#[graphql(parse_token(String, i32))]
|
|
||||||
enum StringOrInt {
|
|
||||||
String(String),
|
|
||||||
Int(i32),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StringOrInt {
|
|
||||||
fn to_output<S>(&self) -> Value<S>
|
|
||||||
where
|
|
||||||
S: ScalarValue,
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Self::String(s) => Value::scalar(s.to_owned()),
|
|
||||||
Self::Int(i) => Value::scalar(*i),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_input<S>(v: &InputValue<S>) -> Result<Self, String>
|
|
||||||
where
|
|
||||||
S: ScalarValue,
|
|
||||||
{
|
|
||||||
v.as_string_value()
|
|
||||||
.map(|s| Self::String(s.into()))
|
|
||||||
.or_else(|| v.as_int_value().map(Self::Int))
|
|
||||||
.ok_or_else(|| format!("Expected `String` or `Int`, found: {v}"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using foreign types as scalars
|
|
||||||
|
|
||||||
For implementing custom scalars on foreign types there is `#[graphql_scalar]` attribute macro.
|
## Foreign
|
||||||
|
|
||||||
> __NOTE:__ To satisfy [orphan rules] you should provide local [`ScalarValue`] implementation.
|
For implementing [custom scalars][2] on foreign types there is [`#[graphql_scalar]`][9] attribute.
|
||||||
|
|
||||||
|
> **NOTE**: To satisfy [orphan rules], we should provide a local [`ScalarValue`] implementation.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
# extern crate juniper;
|
||||||
|
@ -391,10 +358,10 @@ use juniper::{graphql_scalar, InputValue, ScalarValue, Value};
|
||||||
with = date_scalar,
|
with = date_scalar,
|
||||||
parse_token(String),
|
parse_token(String),
|
||||||
scalar = CustomScalarValue,
|
scalar = CustomScalarValue,
|
||||||
// ^^^^^^^^^^^^^^^^^ Local `ScalarValue` implementation.
|
|
||||||
)]
|
)]
|
||||||
|
// ^^^^^^^^^^^^^^^^^ local `ScalarValue` implementation
|
||||||
type Date = date::Date;
|
type Date = date::Date;
|
||||||
// ^^^^^^^^^^ Type from another crate.
|
// ^^^^^^^^^^ type from another crate
|
||||||
|
|
||||||
mod date_scalar {
|
mod date_scalar {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -413,5 +380,103 @@ mod date_scalar {
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Supported out-of-the-box
|
||||||
|
|
||||||
|
[Juniper] provides out-of-the-box [GraphQL scalar][0] implementations for some very common [Rust] crates. The types from these crates will be usable in your schemas automatically after enabling the correspondent self-titled [Cargo feature].
|
||||||
|
|
||||||
|
| [Rust] type | [GraphQL] scalar | [Cargo feature] |
|
||||||
|
|-----------------------------|-----------------------|------------------|
|
||||||
|
| [`bigdecimal::BigDecimal`] | `BigDecimal` | [`bigdecimal`] |
|
||||||
|
| [`bson::oid::ObjectId`] | [`ObjectID`] | [`bson`] |
|
||||||
|
| [`bson::DateTime`] | [`DateTime`] | [`bson`] |
|
||||||
|
| [`chrono::NaiveDate`] | [`LocalDate`] | [`chrono`] |
|
||||||
|
| [`chrono::NaiveTime`] | [`LocalTime`] | [`chrono`] |
|
||||||
|
| [`chrono::NaiveDateTime`] | [`LocalDateTime`] | [`chrono`] |
|
||||||
|
| [`chrono::DateTime`] | [`DateTime`] | [`chrono`] |
|
||||||
|
| [`chrono_tz::Tz`] | [`TimeZone`] | [`chrono-tz`] |
|
||||||
|
| [`rust_decimal::Decimal`] | `Decimal` | [`rust_decimal`] |
|
||||||
|
| [`jiff::civil::Date`] | [`LocalDate`] | [`jiff`] |
|
||||||
|
| [`jiff::civil::Time`] | [`LocalTime`] | [`jiff`] |
|
||||||
|
| [`jiff::civil::DateTime`] | [`LocalDateTime`] | [`jiff`] |
|
||||||
|
| [`jiff::Timestamp`] | [`DateTime`] | [`jiff`] |
|
||||||
|
| [`jiff::Zoned`] | `ZonedDateTime` | [`jiff`] |
|
||||||
|
| [`jiff::tz::TimeZone`] | `TimeZoneOrUtcOffset` | [`jiff`] |
|
||||||
|
| [`jiff::tz::TimeZone`] via [`juniper::integrations::jiff::TimeZone`] | [`TimeZone`] | [`jiff`] |
|
||||||
|
| [`jiff::tz::Offset`] | [`UtcOffset`] | [`jiff`] |
|
||||||
|
| [`jiff::Span`] | [`Duration`] | [`jiff`] |
|
||||||
|
| [`time::Date`] | [`LocalDate`] | [`time`] |
|
||||||
|
| [`time::Time`] | [`LocalTime`] | [`time`] |
|
||||||
|
| [`time::PrimitiveDateTime`] | [`LocalDateTime`] | [`time`] |
|
||||||
|
| [`time::OffsetDateTime`] | [`DateTime`] | [`time`] |
|
||||||
|
| [`time::UtcOffset`] | [`UtcOffset`] | [`time`] |
|
||||||
|
| [`url::Url`] | [`URL`] | [`url`] |
|
||||||
|
| [`uuid::Uuid`] | [`UUID`] | [`uuid`] |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[`bigdecimal`]: https://docs.rs/bigdecimal
|
||||||
|
[`bigdecimal::BigDecimal`]: https://docs.rs/bigdecimal/latest/bigdecimal/struct.BigDecimal.html
|
||||||
|
[`bson`]: https://docs.rs/bson
|
||||||
|
[`bson::DateTime`]: https://docs.rs/bson/latest/bson/struct.DateTime.html
|
||||||
|
[`bson::oid::ObjectId`]: https://docs.rs/bson/latest/bson/oid/struct.ObjectId.html
|
||||||
|
[`chrono`]: https://docs.rs/chrono
|
||||||
|
[`chrono::DateTime`]: https://docs.rs/chrono/latest/chrono/struct.DateTime.html
|
||||||
|
[`chrono::NaiveDate`]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDate.html
|
||||||
|
[`chrono::NaiveDateTime`]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDateTime.html
|
||||||
|
[`chrono::NaiveTime`]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html
|
||||||
|
[`chrono-tz`]: https://docs.rs/chrono-tz
|
||||||
|
[`chrono_tz::Tz`]: https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html
|
||||||
|
[`DateTime`]: https://graphql-scalars.dev/docs/scalars/date-time
|
||||||
|
[`Duration`]: https://graphql-scalars.dev/docs/scalars/duration
|
||||||
|
[`ID`]: https://spec.graphql.org/October2021#sec-ID
|
||||||
|
[`jiff`]: https://docs.rs/jiff
|
||||||
|
[`jiff::civil::Date`]: https://docs.rs/jiff/latest/jiff/civil/struct.Date.html
|
||||||
|
[`jiff::civil::DateTime`]: https://docs.rs/jiff/latest/jiff/civil/struct.DateTime.html
|
||||||
|
[`jiff::civil::Time`]: https://docs.rs/jiff/latest/jiff/civil/struct.Time.html
|
||||||
|
[`jiff::Span`]: https://docs.rs/jiff/latest/jiff/struct.Span.html
|
||||||
|
[`jiff::Timestamp`]: https://docs.rs/jiff/latest/jiff/struct.Timestamp.html
|
||||||
|
[`jiff::tz::Offset`]: https://docs.rs/jiff/latest/jiff/tz/struct.Offset.html
|
||||||
|
[`jiff::tz::TimeZone`]: https://docs.rs/jiff/latest/jiff/tz/struct.TimeZone.html
|
||||||
|
[`jiff::Zoned`]: https://docs.rs/jiff/latest/jiff/struct.Zoned.html
|
||||||
|
[`juniper::integrations::jiff::TimeZone`]: https://docs.rs/juniper/0.16.1/juniper/integrations/jiff/struct.TimeZone.html
|
||||||
|
[`LocalDate`]: https://graphql-scalars.dev/docs/scalars/local-date
|
||||||
|
[`LocalDateTime`]: https://graphql-scalars.dev/docs/scalars/local-date-time
|
||||||
|
[`LocalTime`]: https://graphql-scalars.dev/docs/scalars/local-time
|
||||||
|
[`ObjectID`]: https://the-guild.dev/graphql/scalars/docs/scalars/object-id
|
||||||
|
[`rust_decimal`]: https://docs.rs/rust_decimal
|
||||||
|
[`rust_decimal::Decimal`]: https://docs.rs/rust_decimal/latest/rust_decimal/struct.Decimal.html
|
||||||
|
[`ScalarValue`]: https://docs.rs/juniper/0.16.1/juniper/trait.ScalarValue.html
|
||||||
|
[`serde`]: https://docs.rs/serde
|
||||||
|
[`time`]: https://docs.rs/time
|
||||||
|
[`time::Date`]: https://docs.rs/time/latest/time/struct.Date.html
|
||||||
|
[`time::PrimitiveDateTime`]: https://docs.rs/time/latest/time/struct.PrimitiveDateTime.html
|
||||||
|
[`time::Time`]: https://docs.rs/time/latest/time/struct.Time.html
|
||||||
|
[`time::UtcOffset`]: https://docs.rs/time/latest/time/struct.UtcOffset.html
|
||||||
|
[`time::OffsetDateTime`]: https://docs.rs/time/latest/time/struct.OffsetDateTime.html
|
||||||
|
[`TimeZone`]: https://graphql-scalars.dev/docs/scalars/time-zone
|
||||||
|
[`url`]: https://docs.rs/url
|
||||||
|
[`url::Url`]: https://docs.rs/url/latest/url/struct.Url.html
|
||||||
|
[`URL`]: https://graphql-scalars.dev/docs/scalars/url
|
||||||
|
[`UtcOffset`]: https://graphql-scalars.dev/docs/scalars/utc-offset
|
||||||
|
[`uuid`]: https://docs.rs/uuid
|
||||||
|
[`uuid::Uuid`]: https://docs.rs/uuid/latest/uuid/struct.Uuid.html
|
||||||
|
[`UUID`]: https://graphql-scalars.dev/docs/scalars/uuid
|
||||||
|
[Cargo feature]: https://doc.rust-lang.org/cargo/reference/features.html
|
||||||
|
[GraphQL]: https://graphql.org
|
||||||
|
[Juniper]: https://docs.rs/juniper
|
||||||
[orphan rules]: https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules
|
[orphan rules]: https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules
|
||||||
[`ScalarValue`]: https://docs.rs/juniper/latest/juniper/trait.ScalarValue.html
|
[Rust]: https://www.rust-lang.org
|
||||||
|
[schema]: https://graphql.org/learn/schema
|
||||||
|
|
||||||
|
[0]: https://spec.graphql.org/October2021#sec-Scalars
|
||||||
|
[1]: https://spec.graphql.org/October2021#sel-FAHXJDCAACKB1qb
|
||||||
|
[2]: https://spec.graphql.org/October2021#sec-Scalars.Custom-Scalars
|
||||||
|
[3]: https://rust-unofficial.github.io/patterns/patterns/behavioural/newtype.html
|
||||||
|
[4]: https://doc.rust-lang.org/reference/items/type-aliases.html
|
||||||
|
[5]: https://spec.graphql.org/October2021/#sec-Scalars.Built-in-Scalars
|
||||||
|
[6]: https://serde.rs/container-attrs.html#transparent
|
||||||
|
[7]: https://spec.graphql.org/October2021#sec-Value-Resolution
|
||||||
|
[8]: https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLScalar.html
|
||||||
|
[9]: https://docs.rs/juniper/0.16.1/juniper/attr.graphql_scalar.html
|
||||||
|
|
|
@ -1,25 +1,17 @@
|
||||||
Unions
|
Unions
|
||||||
======
|
======
|
||||||
|
|
||||||
From the server's point of view, [GraphQL unions][1] are somewhat similar to [interfaces][5] - the main difference is that they don't contain fields on their own.
|
> [GraphQL unions][0] represent an object that could be one of a list of [GraphQL object][10] types, but provides for no guaranteed fields between those types. They also differ from [interfaces][12] in that [object][10] types declare what [interfaces][12] they implement, but are not aware of what [unions][0] contain them.
|
||||||
|
|
||||||
The most obvious and straightforward way to represent a [GraphQL union][1] in Rust is enum. However, we also can do so either with trait or a regular struct. That's why, for implementing [GraphQL unions][1] Juniper provides:
|
From the server's point of view, [GraphQL unions][0] are somewhat similar to [interfaces][12]: the main difference is that they don't contain fields on their own, and so, we only need to represent a value, _dispatchable_ into concrete [objects][10].
|
||||||
- `#[derive(GraphQLUnion)]` macro for enums and structs.
|
|
||||||
- `#[graphql_union]` for traits.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Enums
|
|
||||||
|
|
||||||
Most of the time, we just need a trivial and straightforward Rust enum to represent a [GraphQL union][1].
|
|
||||||
|
|
||||||
|
Obviously, the most straightforward approach to express [GraphQL unions][0] in [Rust] is to use [enums][22]. In [Juniper] this may be done by using [`#[derive(GraphQLInterface)]`][2] attribute on them:
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
|
||||||
# extern crate derive_more;
|
# extern crate derive_more;
|
||||||
use derive_more::From;
|
# extern crate juniper;
|
||||||
use juniper::{GraphQLObject, GraphQLUnion};
|
# use derive_more::From;
|
||||||
|
# use juniper::{GraphQLObject, GraphQLUnion};
|
||||||
|
#
|
||||||
#[derive(GraphQLObject)]
|
#[derive(GraphQLObject)]
|
||||||
struct Human {
|
struct Human {
|
||||||
id: String,
|
id: String,
|
||||||
|
@ -33,6 +25,7 @@ struct Droid {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(From, GraphQLUnion)]
|
#[derive(From, GraphQLUnion)]
|
||||||
|
// ^^^^ only for convenience, and may be omitted
|
||||||
enum Character {
|
enum Character {
|
||||||
Human(Human),
|
Human(Human),
|
||||||
Droid(Droid),
|
Droid(Droid),
|
||||||
|
@ -42,22 +35,93 @@ enum Character {
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Ignoring enum variants
|
### Renaming
|
||||||
|
|
||||||
In some rare situations we may want to omit exposing an enum variant in the GraphQL schema.
|
Just as with [renaming GraphQL objects](objects/index.md#renaming), we can override the default [union][0] name by using the `#[graphql(name = "...")]` attribute:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::{GraphQLObject, GraphQLUnion};
|
||||||
|
#
|
||||||
|
# #[derive(GraphQLObject)]
|
||||||
|
# struct Human {
|
||||||
|
# id: String,
|
||||||
|
# home_planet: String,
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# #[derive(GraphQLObject)]
|
||||||
|
# struct Droid {
|
||||||
|
# id: String,
|
||||||
|
# primary_function: String,
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
#[derive(GraphQLUnion)]
|
||||||
|
#[graphql(name = "CharacterUnion")]
|
||||||
|
enum Character { // exposed as `CharacterUnion` in GraphQL schema
|
||||||
|
Human(Human),
|
||||||
|
Droid(Droid),
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
> **NOTE**: Unlike [Rust enum variants][22], [GraphQL union members][0] don't have any special names aside from the ones provided by [objects][10] themselves, and so, obviously, **cannot be renamed**.
|
||||||
|
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
Similarly to [documenting GraphQL objects](objects/index.md#documentation), we can [document][7] a [GraphQL union][0] via `#[graphql(description = "...")]` attribute or [Rust doc comments][6]:
|
||||||
|
```rust
|
||||||
|
# extern crate juniper;
|
||||||
|
# use juniper::{GraphQLObject, GraphQLUnion};
|
||||||
|
#
|
||||||
|
# #[derive(GraphQLObject)]
|
||||||
|
# struct Human {
|
||||||
|
# id: String,
|
||||||
|
# home_planet: String,
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# #[derive(GraphQLObject)]
|
||||||
|
# struct Droid {
|
||||||
|
# id: String,
|
||||||
|
# primary_function: String,
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
/// This doc comment is visible in both Rust API docs and GraphQL schema
|
||||||
|
/// descriptions.
|
||||||
|
#[derive(GraphQLUnion)]
|
||||||
|
enum Character {
|
||||||
|
/// This doc comment is visible only in Rust API docs.
|
||||||
|
Human(Human),
|
||||||
|
/// This doc comment is visible only in Rust API docs.
|
||||||
|
Droid(Droid),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This doc comment is visible only in Rust API docs.
|
||||||
|
#[derive(GraphQLUnion)]
|
||||||
|
#[graphql(description = "This description overwrites the one from doc comment.")]
|
||||||
|
// ^^^^^^^^^^^ or `desc` shortcut, up to your preference
|
||||||
|
enum Person {
|
||||||
|
/// This doc comment is visible only in Rust API docs.
|
||||||
|
Human(Human),
|
||||||
|
}
|
||||||
|
#
|
||||||
|
# fn main() {}
|
||||||
|
```
|
||||||
|
> **NOTE**: Unlike [Rust enum variants][22], [GraphQL union members][0] don't have any special constructors aside from the provided [objects][10] directly, and so, **cannot be [documented][7]**, but rather reuse [object descriptions][7] "as is".
|
||||||
|
|
||||||
|
|
||||||
|
### Ignoring
|
||||||
|
|
||||||
|
In some rare situations we may want to omit exposing an [enum][22] variant in a [GraphQL schema][1]. [Similarly to GraphQL enums](enums.md#ignoring), we can just annotate the variant with the `#[graphql(ignore)]` attribute.
|
||||||
|
|
||||||
As an example, let's consider the situation where we need to bind some type parameter `T` for doing interesting type-level stuff in our resolvers. To achieve this we need to have `PhantomData<T>`, but we don't want it exposed in the GraphQL schema.
|
As an example, let's consider the situation where we need to bind some type parameter `T` for doing interesting type-level stuff in our resolvers. To achieve this we need to have `PhantomData<T>`, but we don't want it exposed in the GraphQL schema.
|
||||||
|
|
||||||
> __WARNING__:
|
|
||||||
> It's the _library user's responsibility_ to ensure that ignored enum variant is _never_ returned from resolvers, otherwise resolving the GraphQL query will __panic at runtime__.
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# extern crate juniper;
|
|
||||||
# extern crate derive_more;
|
# extern crate derive_more;
|
||||||
|
# extern crate juniper;
|
||||||
# use std::marker::PhantomData;
|
# use std::marker::PhantomData;
|
||||||
use derive_more::From;
|
# use derive_more::From;
|
||||||
use juniper::{GraphQLObject, GraphQLUnion};
|
# use juniper::{GraphQLObject, GraphQLUnion};
|
||||||
|
#
|
||||||
#[derive(GraphQLObject)]
|
#[derive(GraphQLObject)]
|
||||||
struct Human {
|
struct Human {
|
||||||
id: String,
|
id: String,
|
||||||
|
@ -75,417 +139,30 @@ enum Character<S> {
|
||||||
Human(Human),
|
Human(Human),
|
||||||
Droid(Droid),
|
Droid(Droid),
|
||||||
#[from(ignore)]
|
#[from(ignore)]
|
||||||
#[graphql(ignore)] // or `#[graphql(skip)]`, your choice
|
#[graphql(ignore)]
|
||||||
|
// ^^^^^^ or `skip`, up to your preference
|
||||||
_State(PhantomData<S>),
|
_State(PhantomData<S>),
|
||||||
}
|
}
|
||||||
#
|
#
|
||||||
# fn main() {}
|
# fn main() {}
|
||||||
```
|
```
|
||||||
|
> **WARNING**: It's the _library user's responsibility_ to ensure that ignored [enum][22] variant is **never** returned from resolvers, otherwise resolving the [GraphQL] query will **panic in runtime**.
|
||||||
|
|
||||||
|
> **TIP**: See more available features in the API docs of the [`#[derive(GraphQLUnion)]`][2] attribute.
|
||||||
|
|
||||||
### External resolver functions
|
|
||||||
|
|
||||||
If some custom logic is needed to resolve a [GraphQL union][1] variant, you may specify an external function to do so:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# #![allow(dead_code)]
|
|
||||||
# extern crate juniper;
|
|
||||||
use juniper::{GraphQLObject, GraphQLUnion};
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
[GraphQL]: https://graphql.org
|
||||||
#[graphql(Context = CustomContext)]
|
[Juniper]: https://docs.rs/juniper
|
||||||
struct Human {
|
[Rust]: https://www.rust-lang.org
|
||||||
id: String,
|
|
||||||
home_planet: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
[0]: https://spec.graphql.org/October2021#sec-Unions
|
||||||
#[graphql(Context = CustomContext)]
|
[1]: https://graphql.org/learn/schema
|
||||||
struct Droid {
|
[2]: https://docs.rs/juniper/0.16.1/juniper/derive.GraphQLUnion.html
|
||||||
id: String,
|
[6]: https://doc.rust-lang.org/reference/comments.html#doc-comments
|
||||||
primary_function: String,
|
[7]: https://spec.graphql.org/October2021#sec-Descriptions
|
||||||
}
|
[10]: https://spec.graphql.org/October2021#sec-Objects
|
||||||
|
[11]: https://spec.graphql.org/October2021#sec-Enums
|
||||||
pub struct CustomContext {
|
[12]: https://spec.graphql.org/October2021#sec-Interfaces
|
||||||
droid: Droid,
|
[22]: https://doc.rust-lang.org/reference/items/enumerations.html#enumerations
|
||||||
}
|
|
||||||
impl juniper::Context for CustomContext {}
|
|
||||||
|
|
||||||
#[derive(GraphQLUnion)]
|
|
||||||
#[graphql(Context = CustomContext)]
|
|
||||||
enum Character {
|
|
||||||
Human(Human),
|
|
||||||
#[graphql(with = Character::droid_from_context)]
|
|
||||||
Droid(Droid),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Character {
|
|
||||||
// NOTICE: The function signature must contain `&self` and `&Context`,
|
|
||||||
// and return `Option<&VariantType>`.
|
|
||||||
fn droid_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Droid> {
|
|
||||||
Some(&ctx.droid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
With an external resolver function we can even declare a new [GraphQL union][1] variant where the Rust type is absent in the initial enum definition. The attribute syntax `#[graphql(on VariantType = resolver_fn)]` follows the [GraphQL syntax for dispatching union variants](https://spec.graphql.org/October2021#example-f8163).
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# #![allow(dead_code)]
|
|
||||||
# extern crate juniper;
|
|
||||||
use juniper::{GraphQLObject, GraphQLUnion};
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(Context = CustomContext)]
|
|
||||||
struct Human {
|
|
||||||
id: String,
|
|
||||||
home_planet: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(Context = CustomContext)]
|
|
||||||
struct Droid {
|
|
||||||
id: String,
|
|
||||||
primary_function: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(Context = CustomContext)]
|
|
||||||
struct Ewok {
|
|
||||||
id: String,
|
|
||||||
is_funny: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct CustomContext {
|
|
||||||
ewok: Ewok,
|
|
||||||
}
|
|
||||||
impl juniper::Context for CustomContext {}
|
|
||||||
|
|
||||||
#[derive(GraphQLUnion)]
|
|
||||||
#[graphql(Context = CustomContext)]
|
|
||||||
#[graphql(on Ewok = Character::ewok_from_context)]
|
|
||||||
enum Character {
|
|
||||||
Human(Human),
|
|
||||||
Droid(Droid),
|
|
||||||
#[graphql(ignore)] // or `#[graphql(skip)]`, your choice
|
|
||||||
Ewok,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Character {
|
|
||||||
fn ewok_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Ewok> {
|
|
||||||
if let Self::Ewok = self {
|
|
||||||
Some(&ctx.ewok)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Structs
|
|
||||||
|
|
||||||
Using Rust structs as [GraphQL unions][1] is very similar to using enums, with the nuance that specifying an external resolver function is the only way to declare a [GraphQL union][1] variant.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use std::collections::HashMap;
|
|
||||||
use juniper::{GraphQLObject, GraphQLUnion};
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(Context = Database)]
|
|
||||||
struct Human {
|
|
||||||
id: String,
|
|
||||||
home_planet: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(Context = Database)]
|
|
||||||
struct Droid {
|
|
||||||
id: String,
|
|
||||||
primary_function: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Database {
|
|
||||||
humans: HashMap<String, Human>,
|
|
||||||
droids: HashMap<String, Droid>,
|
|
||||||
}
|
|
||||||
impl juniper::Context for Database {}
|
|
||||||
|
|
||||||
#[derive(GraphQLUnion)]
|
|
||||||
#[graphql(
|
|
||||||
Context = Database,
|
|
||||||
on Human = Character::get_human,
|
|
||||||
on Droid = Character::get_droid,
|
|
||||||
)]
|
|
||||||
struct Character {
|
|
||||||
id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Character {
|
|
||||||
fn get_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human>{
|
|
||||||
ctx.humans.get(&self.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid>{
|
|
||||||
ctx.droids.get(&self.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Traits
|
|
||||||
|
|
||||||
To use a Rust trait definition as a [GraphQL union][1] you need to use the `#[graphql_union]` macro. [Rust doesn't allow derive macros on traits](https://doc.rust-lang.org/stable/reference/procedural-macros.html#derive-macros), so using `#[derive(GraphQLUnion)]` on traits doesn't work.
|
|
||||||
|
|
||||||
> __NOTICE__:
|
|
||||||
> A __trait has to be [object safe](https://doc.rust-lang.org/stable/reference/items/traits.html#object-safety)__, because schema resolvers will need to return a [trait object](https://doc.rust-lang.org/stable/reference/types/trait-object.html) to specify a [GraphQL union][1] behind it.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
use juniper::{graphql_union, GraphQLObject};
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
struct Human {
|
|
||||||
id: String,
|
|
||||||
home_planet: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
struct Droid {
|
|
||||||
id: String,
|
|
||||||
primary_function: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[graphql_union]
|
|
||||||
trait Character {
|
|
||||||
// NOTICE: The method signature must contain `&self` and return `Option<&VariantType>`.
|
|
||||||
fn as_human(&self) -> Option<&Human> { None }
|
|
||||||
fn as_droid(&self) -> Option<&Droid> { None }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Character for Human {
|
|
||||||
fn as_human(&self) -> Option<&Human> { Some(&self) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Character for Droid {
|
|
||||||
fn as_droid(&self) -> Option<&Droid> { Some(&self) }
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Custom context
|
|
||||||
|
|
||||||
If a [`Context`][6] is required in a trait method to resolve a [GraphQL union][1] variant, specify it as an argument.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# #![allow(unused_variables)]
|
|
||||||
# extern crate juniper;
|
|
||||||
# use std::collections::HashMap;
|
|
||||||
use juniper::{graphql_union, GraphQLObject};
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(Context = Database)]
|
|
||||||
struct Human {
|
|
||||||
id: String,
|
|
||||||
home_planet: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(Context = Database)]
|
|
||||||
struct Droid {
|
|
||||||
id: String,
|
|
||||||
primary_function: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Database {
|
|
||||||
humans: HashMap<String, Human>,
|
|
||||||
droids: HashMap<String, Droid>,
|
|
||||||
}
|
|
||||||
impl juniper::Context for Database {}
|
|
||||||
|
|
||||||
#[graphql_union(context = Database)]
|
|
||||||
trait Character {
|
|
||||||
// NOTICE: The method signature may optionally contain `&Context`.
|
|
||||||
fn as_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> { None }
|
|
||||||
fn as_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid> { None }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Character for Human {
|
|
||||||
fn as_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> {
|
|
||||||
ctx.humans.get(&self.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Character for Droid {
|
|
||||||
fn as_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid> {
|
|
||||||
ctx.droids.get(&self.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Ignoring trait methods
|
|
||||||
|
|
||||||
As with enums, we may want to omit some trait methods to be assumed as [GraphQL union][1] variants and ignore them.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
use juniper::{graphql_union, GraphQLObject};
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
struct Human {
|
|
||||||
id: String,
|
|
||||||
home_planet: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
struct Droid {
|
|
||||||
id: String,
|
|
||||||
primary_function: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[graphql_union]
|
|
||||||
trait Character {
|
|
||||||
fn as_human(&self) -> Option<&Human> { None }
|
|
||||||
fn as_droid(&self) -> Option<&Droid> { None }
|
|
||||||
#[graphql(ignore)] // or `#[graphql(skip)]`, your choice
|
|
||||||
fn id(&self) -> &str;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Character for Human {
|
|
||||||
fn as_human(&self) -> Option<&Human> { Some(&self) }
|
|
||||||
fn id(&self) -> &str { self.id.as_str() }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Character for Droid {
|
|
||||||
fn as_droid(&self) -> Option<&Droid> { Some(&self) }
|
|
||||||
fn id(&self) -> &str { self.id.as_str() }
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### External resolver functions
|
|
||||||
|
|
||||||
Similarly to enums and structs, it's not mandatory to use trait methods as [GraphQL union][1] variant resolvers. Instead, custom functions may be specified:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# extern crate juniper;
|
|
||||||
# use std::collections::HashMap;
|
|
||||||
use juniper::{graphql_union, GraphQLObject};
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(Context = Database)]
|
|
||||||
struct Human {
|
|
||||||
id: String,
|
|
||||||
home_planet: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(Context = Database)]
|
|
||||||
struct Droid {
|
|
||||||
id: String,
|
|
||||||
primary_function: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Database {
|
|
||||||
humans: HashMap<String, Human>,
|
|
||||||
droids: HashMap<String, Droid>,
|
|
||||||
}
|
|
||||||
impl juniper::Context for Database {}
|
|
||||||
|
|
||||||
#[graphql_union(context = Database)]
|
|
||||||
#[graphql_union(
|
|
||||||
on Human = DynCharacter::get_human,
|
|
||||||
on Droid = get_droid,
|
|
||||||
)]
|
|
||||||
trait Character {
|
|
||||||
#[graphql(ignore)] // or `#[graphql(skip)]`, your choice
|
|
||||||
fn id(&self) -> &str;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Character for Human {
|
|
||||||
fn id(&self) -> &str { self.id.as_str() }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Character for Droid {
|
|
||||||
fn id(&self) -> &str { self.id.as_str() }
|
|
||||||
}
|
|
||||||
|
|
||||||
// The trait object is always `Send` and `Sync`.
|
|
||||||
type DynCharacter<'a> = dyn Character + Send + Sync + 'a;
|
|
||||||
|
|
||||||
impl<'a> DynCharacter<'a> {
|
|
||||||
fn get_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> {
|
|
||||||
ctx.humans.get(self.id())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// External resolver function doesn't have to be a method of a type.
|
|
||||||
// It's only a matter of the function signature to match the requirements.
|
|
||||||
fn get_droid<'db>(ch: &DynCharacter<'_>, ctx: &'db Database) -> Option<&'db Droid> {
|
|
||||||
ctx.droids.get(ch.id())
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## `ScalarValue` considerations
|
|
||||||
|
|
||||||
By default, `#[derive(GraphQLUnion)]` and `#[graphql_union]` macros generate code, which is generic over a [`ScalarValue`][2] type. This may introduce a problem when at least one of [GraphQL union][1] variants is restricted to a concrete [`ScalarValue`][2] type in its implementation. To resolve such problem, a concrete [`ScalarValue`][2] type should be specified:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# #![allow(dead_code)]
|
|
||||||
# extern crate juniper;
|
|
||||||
use juniper::{DefaultScalarValue, GraphQLObject, GraphQLUnion};
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
#[graphql(Scalar = DefaultScalarValue)]
|
|
||||||
struct Human {
|
|
||||||
id: String,
|
|
||||||
home_planet: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLObject)]
|
|
||||||
struct Droid {
|
|
||||||
id: String,
|
|
||||||
primary_function: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(GraphQLUnion)]
|
|
||||||
#[graphql(Scalar = DefaultScalarValue)] // removing this line will fail compilation
|
|
||||||
enum Character {
|
|
||||||
Human(Human),
|
|
||||||
Droid(Droid),
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# fn main() {}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[1]: https://spec.graphql.org/October2021#sec-Unions
|
|
||||||
[2]: https://docs.rs/juniper/latest/juniper/trait.ScalarValue.html
|
|
||||||
[5]: https://spec.graphql.org/October2021#sec-Interfaces
|
|
||||||
[6]: https://docs.rs/juniper/0.14.2/juniper/trait.Context.html
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "example_actix_subscriptions"
|
|
||||||
version = "0.0.0"
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.62"
|
|
||||||
authors = ["Mihai Dinculescu <mihai.dinculescu@outlook.com>"]
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
actix-cors = "0.6"
|
|
||||||
actix-web = "4.0"
|
|
||||||
async-stream = "0.3"
|
|
||||||
env_logger = "0.9"
|
|
||||||
futures = "0.3"
|
|
||||||
juniper = { path = "../../juniper", features = ["expose-test-schema"] }
|
|
||||||
juniper_actix = { path = "../../juniper_actix", features = ["subscriptions"] }
|
|
||||||
juniper_graphql_ws = { path = "../../juniper_graphql_ws" }
|
|
||||||
rand = "0.8"
|
|
||||||
serde = "1.0"
|
|
||||||
serde_json = "1.0"
|
|
||||||
tokio = "1.0"
|
|
|
@ -1,15 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "example_basic_subscriptions"
|
|
||||||
version = "0.0.0"
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.62"
|
|
||||||
authors = ["Jordao Rosario <jordao.rosario01@gmail.com>"]
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
futures = "0.3"
|
|
||||||
juniper = { path = "../../juniper" }
|
|
||||||
juniper_subscriptions = { path = "../../juniper_subscriptions" }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
|
|
|
@ -1,17 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "example_warp_async"
|
|
||||||
version = "0.0.0"
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.62"
|
|
||||||
authors = ["Christoph Herzog <chris@theduke.at>"]
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
env_logger = "0.9"
|
|
||||||
futures = "0.3"
|
|
||||||
juniper = { path = "../../juniper" }
|
|
||||||
juniper_warp = { path = "../../juniper_warp" }
|
|
||||||
log = "0.4"
|
|
||||||
reqwest = { version = "0.11", features = ["rustls-tls"] }
|
|
||||||
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
|
|
||||||
warp = "0.3"
|
|
|
@ -1,104 +0,0 @@
|
||||||
//! This example demonstrates async/await usage with warp.
|
|
||||||
|
|
||||||
use juniper::{
|
|
||||||
graphql_object, EmptyMutation, EmptySubscription, FieldError, GraphQLEnum, RootNode,
|
|
||||||
};
|
|
||||||
use warp::{http::Response, Filter};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
struct Context;
|
|
||||||
impl juniper::Context for Context {}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, GraphQLEnum)]
|
|
||||||
enum UserKind {
|
|
||||||
Admin,
|
|
||||||
User,
|
|
||||||
Guest,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
struct User {
|
|
||||||
id: i32,
|
|
||||||
kind: UserKind,
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[graphql_object(context = Context)]
|
|
||||||
impl User {
|
|
||||||
fn id(&self) -> i32 {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn kind(&self) -> UserKind {
|
|
||||||
self.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn friends(&self) -> Vec<User> {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
struct Query;
|
|
||||||
|
|
||||||
#[graphql_object(context = Context)]
|
|
||||||
impl Query {
|
|
||||||
async fn users() -> Vec<User> {
|
|
||||||
vec![User {
|
|
||||||
id: 1,
|
|
||||||
kind: UserKind::Admin,
|
|
||||||
name: "user1".into(),
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch a URL and return the response body text.
|
|
||||||
async fn request(url: String) -> Result<String, FieldError> {
|
|
||||||
Ok(reqwest::get(&url).await?.text().await?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Schema = RootNode<'static, Query, EmptyMutation<Context>, EmptySubscription<Context>>;
|
|
||||||
|
|
||||||
fn schema() -> Schema {
|
|
||||||
Schema::new(
|
|
||||||
Query,
|
|
||||||
EmptyMutation::<Context>::new(),
|
|
||||||
EmptySubscription::<Context>::new(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
std::env::set_var("RUST_LOG", "warp_async");
|
|
||||||
env_logger::init();
|
|
||||||
|
|
||||||
let log = warp::log("warp_server");
|
|
||||||
|
|
||||||
let homepage = warp::path::end().map(|| {
|
|
||||||
Response::builder()
|
|
||||||
.header("content-type", "text/html")
|
|
||||||
.body(
|
|
||||||
"<html><h1>juniper_warp</h1><div>visit <a href=\"/graphiql\">/graphiql</a></html>",
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
log::info!("Listening on 127.0.0.1:8080");
|
|
||||||
|
|
||||||
let state = warp::any().map(|| Context);
|
|
||||||
let graphql_filter = juniper_warp::make_graphql_filter(schema(), state.boxed());
|
|
||||||
|
|
||||||
warp::serve(
|
|
||||||
warp::get()
|
|
||||||
.and(warp::path("graphiql"))
|
|
||||||
.and(juniper_warp::graphiql_filter("/graphql", None))
|
|
||||||
.or(homepage)
|
|
||||||
.or(warp::path("graphql").and(graphql_filter))
|
|
||||||
.with(log),
|
|
||||||
)
|
|
||||||
.run(([127, 0, 0, 1], 8080))
|
|
||||||
.await
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "example_warp_subscriptions"
|
|
||||||
version = "0.0.0"
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.62"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
async-stream = "0.3"
|
|
||||||
env_logger = "0.9"
|
|
||||||
futures = "0.3"
|
|
||||||
juniper = { path = "../../juniper" }
|
|
||||||
juniper_graphql_ws = { path = "../../juniper_graphql_ws" }
|
|
||||||
juniper_warp = { path = "../../juniper_warp", features = ["subscriptions"] }
|
|
||||||
log = "0.4.8"
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
|
||||||
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
|
|
||||||
warp = "0.3"
|
|
3
juniper/.gitignore
vendored
Normal file
3
juniper/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/node_modules/
|
||||||
|
/package-lock.json
|
||||||
|
/yarn.lock
|
|
@ -8,7 +8,78 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
|
||||||
|
|
||||||
## master
|
## master
|
||||||
|
|
||||||
[Diff](/../../compare/juniper-v0.15.9...master)
|
[Diff](/../../compare/juniper-v0.16.1...master) | [Milestone](/../../milestone/7)
|
||||||
|
|
||||||
|
### BC Breaks
|
||||||
|
|
||||||
|
- Upgraded [`chrono-tz` crate] integration to [0.10 version](https://github.com/chronotope/chrono-tz/releases/tag/v0.10.0). ([#1252], [#1284])
|
||||||
|
- Bumped up [MSRV] to 1.75. ([#1272])
|
||||||
|
- Corrected compliance with newer [graphql-scalars.dev] specs: ([#1275], [#1277])
|
||||||
|
- Switched `LocalDateTime` scalars to `yyyy-MM-ddTHH:mm:ss` format in types:
|
||||||
|
- `chrono::NaiveDateTime`.
|
||||||
|
- `time::PrimitiveDateTime`.
|
||||||
|
- Switched from `Date` scalar to `LocalDate` scalar in types:
|
||||||
|
- `chrono::NaiveDate`.
|
||||||
|
- `time::Date`.
|
||||||
|
- Switched from `UtcDateTime` scalar to `DateTime` scalar in types:
|
||||||
|
- `bson::DateTime`.
|
||||||
|
- Corrected `TimeZone` scalar in types:
|
||||||
|
- `chrono_tz::Tz`.
|
||||||
|
- Renamed `Url` scalar to `URL` in types:
|
||||||
|
- `url::Url`.
|
||||||
|
- Renamed `Uuid` scalar to `UUID` in types:
|
||||||
|
- `uuid::Uuid`.
|
||||||
|
- Renamed `ObjectId` scalar to `ObjectID` in types: ([#1277])
|
||||||
|
- `bson::oid::ObjectId`.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- [`jiff` crate] integration behind `jiff` [Cargo feature]: ([#1271], [#1278], [#1270])
|
||||||
|
- `jiff::civil::Date` as `LocalDate` scalar.
|
||||||
|
- `jiff::civil::Time` as `LocalTime` scalar.
|
||||||
|
- `jiff::civil::DateTime` as `LocalDateTime` scalar. ([#1275])
|
||||||
|
- `jiff::Timestamp` as `DateTime` scalar.
|
||||||
|
- `jiff::Zoned` as `ZonedDateTime` scalar.
|
||||||
|
- `jiff::tz::TimeZone` as `TimeZoneOrUtcOffset` and `TimeZone` scalars.
|
||||||
|
- `jiff::tz::Offset` as `UtcOffset` scalar.
|
||||||
|
- `jiff::Span` as `Duration` scalar.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated [GraphiQL] to [3.8.3 version](https://github.com/graphql/graphiql/blob/graphiql%403.8.3/packages/graphiql/CHANGELOG.md#383). ([#1300])
|
||||||
|
|
||||||
|
[#1252]: /../../pull/1252
|
||||||
|
[#1270]: /../../issues/1270
|
||||||
|
[#1271]: /../../pull/1271
|
||||||
|
[#1272]: /../../pull/1272
|
||||||
|
[#1275]: /../../pull/1275
|
||||||
|
[#1277]: /../../pull/1277
|
||||||
|
[#1278]: /../../pull/1278
|
||||||
|
[#1281]: /../../pull/1281
|
||||||
|
[#1284]: /../../pull/1284
|
||||||
|
[#1300]: /../../pull/1300
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.16.1] · 2024-04-04
|
||||||
|
[0.16.1]: /../../tree/juniper-v0.16.1/juniper
|
||||||
|
|
||||||
|
[Diff](/../../compare/juniper-v0.16.0...juniper-v0.16.1) | [Milestone](/../../milestone/6)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated [GraphiQL] to 3.1.2 version. ([#1251])
|
||||||
|
|
||||||
|
[#1251]: /../../pull/1251
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.16.0] · 2024-03-20
|
||||||
|
[0.16.0]: /../../tree/juniper-v0.16.0/juniper
|
||||||
|
|
||||||
|
[Diff](/../../compare/juniper-v0.15.12...juniper-v0.16.0) | [Milestone](/../../milestone/4)
|
||||||
|
|
||||||
### BC Breaks
|
### BC Breaks
|
||||||
|
|
||||||
|
@ -22,6 +93,8 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
|
||||||
- Renamed `rename = "<policy>"` attribute argument to `rename_all = "<policy>"` (following `serde` style). ([#971])
|
- Renamed `rename = "<policy>"` attribute argument to `rename_all = "<policy>"` (following `serde` style). ([#971])
|
||||||
- Upgraded [`bson` crate] integration to [2.0 version](https://github.com/mongodb/bson-rust/releases/tag/v2.0.0). ([#979])
|
- Upgraded [`bson` crate] integration to [2.0 version](https://github.com/mongodb/bson-rust/releases/tag/v2.0.0). ([#979])
|
||||||
- Upgraded [`uuid` crate] integration to [1.0 version](https://github.com/uuid-rs/uuid/releases/tag/1.0.0). ([#1057])
|
- Upgraded [`uuid` crate] integration to [1.0 version](https://github.com/uuid-rs/uuid/releases/tag/1.0.0). ([#1057])
|
||||||
|
- Upgraded [`chrono-tz` crate] integration to [0.8 version](https://github.com/chronotope/chrono-tz/blob/ea628d3131b4a659acb42dbac885cfd08a2e5de9/CHANGELOG.md#080). ([#1119])
|
||||||
|
- Upgraded [`bigdecimal` crate] integration to 0.4 version. ([#1176])
|
||||||
- Made `FromInputValue` trait methods fallible to allow post-validation. ([#987])
|
- Made `FromInputValue` trait methods fallible to allow post-validation. ([#987])
|
||||||
- Redesigned `#[graphql_interface]` macro: ([#1009])
|
- Redesigned `#[graphql_interface]` macro: ([#1009])
|
||||||
- Removed support for `dyn` attribute argument (interface values as trait objects).
|
- Removed support for `dyn` attribute argument (interface values as trait objects).
|
||||||
|
@ -49,6 +122,24 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
|
||||||
- Disabled `chrono` [Cargo feature] by default.
|
- Disabled `chrono` [Cargo feature] by default.
|
||||||
- Removed `scalar-naivetime` [Cargo feature].
|
- Removed `scalar-naivetime` [Cargo feature].
|
||||||
- Removed lifetime parameter from `ParseError`, `GraphlQLError`, `GraphQLBatchRequest` and `GraphQLRequest`. ([#1081], [#528])
|
- Removed lifetime parameter from `ParseError`, `GraphlQLError`, `GraphQLBatchRequest` and `GraphQLRequest`. ([#1081], [#528])
|
||||||
|
- Upgraded [GraphiQL] to 3.1.1 version (requires new [`graphql-transport-ws` GraphQL over WebSocket Protocol] integration on server, see `juniper_warp/examples/subscription.rs`). ([#1188], [#1193], [#1246])
|
||||||
|
- Abstracted `Spanning::start` and `Spanning::end` fields into separate struct `Span`. ([#1207], [#1208])
|
||||||
|
- Removed `graphql-parser-integration` and `graphql-parser` [Cargo feature]s by merging them into `schema-language` [Cargo feature]. ([#1237])
|
||||||
|
- Renamed `RootNode::as_schema_language()` method as `RootNode::as_sdl()`. ([#1237])
|
||||||
|
- Renamed `RootNode::as_parser_document()` method as `RootNode::as_document()`. ([#1237])
|
||||||
|
- Reworked look-ahead machinery: ([#1212])
|
||||||
|
- Turned from eagerly-evaluated into lazy-evaluated:
|
||||||
|
- Made `LookAheadValue::List` to contain new iterable `LookAheadList` type.
|
||||||
|
- Made `LookAheadValue::Object` to contain new iterable `LookAheadObject` type.
|
||||||
|
- Removed `LookAheadMethods` trait and redundant `ConcreteLookAheadSelection` type, making all APIs accessible as inherent methods on `LookAheadSelection` and `LookAheadChildren` decoupled types:
|
||||||
|
- Moved `LookAheadMethods::child_names()` to `LookAheadChildren::names()`.
|
||||||
|
- Moved `LookAheadMethods::has_children()` to `LookAheadChildren::is_empty()`.
|
||||||
|
- Moved `LookAheadMethods::select_child()` to `LookAheadChildren::select()`.
|
||||||
|
- Moved `LookAheadSelection::for_explicit_type()` to `LookAheadSelection::children_for_explicit_type()`.
|
||||||
|
- Made `LookAheadSelection::arguments()` returning iterator over `LookAheadArgument`.
|
||||||
|
- Made `LookAheadSelection::children()` returning `LookAheadChildren`.
|
||||||
|
- Added `Span` to `Arguments` and `LookAheadArguments`. ([#1206], [#1209])
|
||||||
|
- Disabled `bson`, `url`, `uuid` and `schema-language` [Cargo feature]s by default. ([#1230])
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
@ -60,11 +151,19 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
|
||||||
- `#[derive(GraphQLInterface)]` macro allowing using structs as GraphQL interfaces. ([#1026])
|
- `#[derive(GraphQLInterface)]` macro allowing using structs as GraphQL interfaces. ([#1026])
|
||||||
- [`bigdecimal` crate] integration behind `bigdecimal` [Cargo feature]. ([#1060])
|
- [`bigdecimal` crate] integration behind `bigdecimal` [Cargo feature]. ([#1060])
|
||||||
- [`rust_decimal` crate] integration behind `rust_decimal` [Cargo feature]. ([#1060])
|
- [`rust_decimal` crate] integration behind `rust_decimal` [Cargo feature]. ([#1060])
|
||||||
|
- `js` [Cargo feature] enabling `js-sys` and `wasm-bindgen` support for `wasm32-unknown-unknown` target. ([#1118], [#1147])
|
||||||
|
- `LookAheadMethods::applies_for()` method. ([#1138], [#1145])
|
||||||
|
- `LookAheadMethods::field_original_name()` and `LookAheadMethods::field_alias()` methods. ([#1199])
|
||||||
|
- [`anyhow` crate] integration behind `anyhow` and `backtrace` [Cargo feature]s. ([#1215], [#988])
|
||||||
|
- `RootNode::disable_introspection()` applying additional `validation::rules::disable_introspection`, and `RootNode::enable_introspection()` reverting it. ([#1227], [#456])
|
||||||
|
- `Clone` and `PartialEq` implementations for `GraphQLResponse`. ([#1228], [#103])
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Made `GraphQLRequest` fields public. ([#750])
|
- Made `GraphQLRequest` fields public. ([#750])
|
||||||
- Relaxed [object safety] requirement for `GraphQLValue` and `GraphQLValueAsync` traits. ([ba1ed85b])
|
- Relaxed [object safety] requirement for `GraphQLValue` and `GraphQLValueAsync` traits. ([ba1ed85b])
|
||||||
|
- Updated [GraphQL Playground] to 1.7.28 version. ([#1190])
|
||||||
|
- Improve validation errors for input values. ([#811], [#693])
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
|
@ -75,12 +174,18 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
|
||||||
- Incorrect input value coercion with defaults. ([#1080], [#1073])
|
- Incorrect input value coercion with defaults. ([#1080], [#1073])
|
||||||
- Incorrect error when explicit `null` provided for `null`able list input parameter. ([#1086], [#1085])
|
- Incorrect error when explicit `null` provided for `null`able list input parameter. ([#1086], [#1085])
|
||||||
- Stack overflow on nested GraphQL fragments. ([CVE-2022-31173])
|
- Stack overflow on nested GraphQL fragments. ([CVE-2022-31173])
|
||||||
|
- Unstable definitions order in schema generated by `RootNode::as_sdl()`. ([#1237], [#1134])
|
||||||
|
- Unstable definitions order in schema generated by `introspect()` or other introspection queries. ([#1239], [#1134])
|
||||||
|
|
||||||
|
[#103]: /../../issues/103
|
||||||
[#113]: /../../issues/113
|
[#113]: /../../issues/113
|
||||||
|
[#456]: /../../issues/456
|
||||||
[#503]: /../../issues/503
|
[#503]: /../../issues/503
|
||||||
[#528]: /../../issues/528
|
[#528]: /../../issues/528
|
||||||
|
[#693]: /../../issues/693
|
||||||
[#750]: /../../issues/750
|
[#750]: /../../issues/750
|
||||||
[#798]: /../../issues/798
|
[#798]: /../../issues/798
|
||||||
|
[#811]: /../../pull/811
|
||||||
[#918]: /../../issues/918
|
[#918]: /../../issues/918
|
||||||
[#965]: /../../pull/965
|
[#965]: /../../pull/965
|
||||||
[#966]: /../../pull/966
|
[#966]: /../../pull/966
|
||||||
|
@ -88,6 +193,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
|
||||||
[#979]: /../../pull/979
|
[#979]: /../../pull/979
|
||||||
[#985]: /../../pull/985
|
[#985]: /../../pull/985
|
||||||
[#987]: /../../pull/987
|
[#987]: /../../pull/987
|
||||||
|
[#988]: /../../issues/988
|
||||||
[#996]: /../../pull/996
|
[#996]: /../../pull/996
|
||||||
[#1000]: /../../issues/1000
|
[#1000]: /../../issues/1000
|
||||||
[#1001]: /../../pull/1001
|
[#1001]: /../../pull/1001
|
||||||
|
@ -111,6 +217,29 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
|
||||||
[#1081]: /../../pull/1081
|
[#1081]: /../../pull/1081
|
||||||
[#1085]: /../../issues/1085
|
[#1085]: /../../issues/1085
|
||||||
[#1086]: /../../pull/1086
|
[#1086]: /../../pull/1086
|
||||||
|
[#1118]: /../../issues/1118
|
||||||
|
[#1119]: /../../pull/1119
|
||||||
|
[#1134]: /../../issues/1134
|
||||||
|
[#1138]: /../../issues/1138
|
||||||
|
[#1145]: /../../pull/1145
|
||||||
|
[#1147]: /../../pull/1147
|
||||||
|
[#1176]: /../../pull/1176
|
||||||
|
[#1188]: /../../pull/1188
|
||||||
|
[#1190]: /../../pull/1190
|
||||||
|
[#1193]: /../../pull/1193
|
||||||
|
[#1199]: /../../pull/1199
|
||||||
|
[#1206]: /../../pull/1206
|
||||||
|
[#1207]: /../../pull/1207
|
||||||
|
[#1208]: /../../pull/1208
|
||||||
|
[#1209]: /../../pull/1209
|
||||||
|
[#1212]: /../../pull/1212
|
||||||
|
[#1215]: /../../pull/1215
|
||||||
|
[#1227]: /../../pull/1227
|
||||||
|
[#1228]: /../../pull/1228
|
||||||
|
[#1230]: /../../pull/1230
|
||||||
|
[#1237]: /../../pull/1237
|
||||||
|
[#1239]: /../../pull/1239
|
||||||
|
[#1246]: /../../pull/1246
|
||||||
[ba1ed85b]: /../../commit/ba1ed85b3c3dd77fbae7baf6bc4e693321a94083
|
[ba1ed85b]: /../../commit/ba1ed85b3c3dd77fbae7baf6bc4e693321a94083
|
||||||
[CVE-2022-31173]: /../../security/advisories/GHSA-4rx6-g5vg-5f3j
|
[CVE-2022-31173]: /../../security/advisories/GHSA-4rx6-g5vg-5f3j
|
||||||
|
|
||||||
|
@ -119,16 +248,24 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
|
||||||
|
|
||||||
## Previous releases
|
## Previous releases
|
||||||
|
|
||||||
See [old CHANGELOG](/../../blob/juniper-v0.15.9/juniper/CHANGELOG.md).
|
See [old CHANGELOG](/../../blob/juniper-v0.15.12/juniper/CHANGELOG.md).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[`anyhow` crate]: https://docs.rs/anyhow
|
||||||
|
[`bigdecimal` crate]: https://docs.rs/bigdecimal
|
||||||
[`bson` crate]: https://docs.rs/bson
|
[`bson` crate]: https://docs.rs/bson
|
||||||
[`chrono` crate]: https://docs.rs/chrono
|
[`chrono` crate]: https://docs.rs/chrono
|
||||||
|
[`chrono-tz` crate]: https://docs.rs/chrono-tz
|
||||||
|
[`jiff` crate]: https://docs.rs/jiff
|
||||||
[`time` crate]: https://docs.rs/time
|
[`time` crate]: https://docs.rs/time
|
||||||
[Cargo feature]: https://doc.rust-lang.org/cargo/reference/features.html
|
[Cargo feature]: https://doc.rust-lang.org/cargo/reference/features.html
|
||||||
|
[`graphql-transport-ws` GraphQL over WebSocket Protocol]: https://github.com/enisdenjo/graphql-ws/v5.14.0/PROTOCOL.md
|
||||||
|
[GraphiQL]: https://github.com/graphql/graphiql
|
||||||
|
[GraphQL Playground]: https://github.com/prisma/graphql-playground
|
||||||
[graphql-scalars.dev]: https://graphql-scalars.dev
|
[graphql-scalars.dev]: https://graphql-scalars.dev
|
||||||
|
[MSRV]: https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field
|
||||||
[October 2021]: https://spec.graphql.org/October2021
|
[October 2021]: https://spec.graphql.org/October2021
|
||||||
[object safety]: https://doc.rust-lang.org/reference/items/traits.html#object-safety
|
[object safety]: https://doc.rust-lang.org/reference/items/traits.html#object-safety
|
||||||
[orphan rules]: https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules
|
[orphan rules]: https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
[package]
|
[package]
|
||||||
name = "juniper"
|
name = "juniper"
|
||||||
version = "0.16.0-dev"
|
version = "0.16.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.62"
|
rust-version = "1.75"
|
||||||
description = "GraphQL server library."
|
description = "GraphQL server library."
|
||||||
license = "BSD-2-Clause"
|
license = "BSD-2-Clause"
|
||||||
authors = [
|
authors = [
|
||||||
|
@ -13,61 +13,81 @@ authors = [
|
||||||
"Kai Ren <tyranron@gmail.com>",
|
"Kai Ren <tyranron@gmail.com>",
|
||||||
]
|
]
|
||||||
documentation = "https://docs.rs/juniper"
|
documentation = "https://docs.rs/juniper"
|
||||||
homepage = "https://graphql-rust.github.io"
|
homepage = "https://graphql-rust.github.io/juniper"
|
||||||
repository = "https://github.com/graphql-rust/juniper"
|
repository = "https://github.com/graphql-rust/juniper"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
categories = ["asynchronous", "web-programming", "web-programming::http-server"]
|
categories = ["asynchronous", "web-programming", "web-programming::http-server"]
|
||||||
keywords = ["apollo", "graphql", "server", "web"]
|
keywords = ["apollo", "graphql", "server", "web"]
|
||||||
exclude = ["/release.toml"]
|
include = ["/src/", "/CHANGELOG.md", "/LICENSE", "/README.md"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [
|
anyhow = ["dep:anyhow"]
|
||||||
"bson",
|
backtrace = ["anyhow?/backtrace"]
|
||||||
"schema-language",
|
bigdecimal = ["dep:bigdecimal", "dep:num-bigint", "dep:ryu"]
|
||||||
"url",
|
bson = ["dep:bson", "dep:tap"]
|
||||||
"uuid",
|
chrono = ["dep:chrono"]
|
||||||
]
|
|
||||||
chrono-clock = ["chrono", "chrono/clock"]
|
chrono-clock = ["chrono", "chrono/clock"]
|
||||||
expose-test-schema = ["anyhow", "serde_json"]
|
chrono-tz = ["dep:chrono-tz", "dep:regex"]
|
||||||
schema-language = ["graphql-parser"]
|
expose-test-schema = ["dep:anyhow", "dep:serde_json"]
|
||||||
|
jiff = ["dep:jiff"]
|
||||||
|
js = ["chrono?/wasmbind", "time?/wasm-bindgen", "uuid?/js"]
|
||||||
|
rust_decimal = ["dep:rust_decimal"]
|
||||||
|
schema-language = ["dep:graphql-parser", "dep:void"]
|
||||||
|
time = ["dep:time"]
|
||||||
|
url = ["dep:url"]
|
||||||
|
uuid = ["dep:uuid"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { version = "1.0.32", default-features = false, optional = true }
|
anyhow = { version = "1.0.47", optional = true }
|
||||||
async-trait = "0.1.39"
|
async-trait = "0.1.39"
|
||||||
bigdecimal = { version = "0.3", optional = true }
|
auto_enums = "0.8"
|
||||||
bson = { version = "2.4", features = ["chrono-0_4"], optional = true }
|
bigdecimal = { version = "0.4", optional = true }
|
||||||
chrono = { version = "0.4.20", features = ["alloc"], default-features = false, optional = true }
|
bson = { version = "2.4", optional = true }
|
||||||
chrono-tz = { version = "0.6", default-features = false, optional = true }
|
chrono = { version = "0.4.30", features = ["alloc"], default-features = false, optional = true }
|
||||||
fnv = "1.0.3"
|
chrono-tz = { version = "0.10", default-features = false, optional = true }
|
||||||
|
fnv = "1.0.5"
|
||||||
futures = { version = "0.3.22", features = ["alloc"], default-features = false }
|
futures = { version = "0.3.22", features = ["alloc"], default-features = false }
|
||||||
futures-enum = { version = "0.1.12", default-features = false }
|
|
||||||
graphql-parser = { version = "0.4", optional = true }
|
graphql-parser = { version = "0.4", optional = true }
|
||||||
indexmap = { version = "1.0", features = ["serde-1"] }
|
indexmap = { version = "2.0", features = ["serde"] }
|
||||||
juniper_codegen = { version = "0.16.0-dev", path = "../juniper_codegen" }
|
jiff = { version = "0.1.16", features = ["std"], default-features = false, optional = true }
|
||||||
rust_decimal = { version = "1.0", default-features = false, optional = true }
|
juniper_codegen = { version = "0.16.0", path = "../juniper_codegen" }
|
||||||
serde = { version = "1.0.8", features = ["derive"] }
|
rust_decimal = { version = "1.20", default-features = false, optional = true }
|
||||||
serde_json = { version = "1.0.2", default-features = false, optional = true }
|
ryu = { version = "1.0", optional = true }
|
||||||
|
serde = { version = "1.0.122", features = ["derive"] }
|
||||||
|
serde_json = { version = "1.0.18", features = ["std"], default-features = false, optional = true }
|
||||||
smartstring = "1.0"
|
smartstring = "1.0"
|
||||||
static_assertions = "1.1"
|
static_assertions = "1.1"
|
||||||
time = { version = "0.3", features = ["formatting", "macros", "parsing"], optional = true }
|
time = { version = "0.3.37", features = ["formatting", "macros", "parsing"], optional = true }
|
||||||
url = { version = "2.0", optional = true }
|
url = { version = "2.0", optional = true }
|
||||||
uuid = { version = "1.0", default-features = false, optional = true }
|
uuid = { version = "1.3", default-features = false, optional = true }
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
# Fixes for `minimal-versions` check.
|
||||||
getrandom = { version = "0.2", features = ["js"] }
|
# TODO: Try remove on upgrade of `bigdecimal` crate.
|
||||||
|
num-bigint = { version = "0.4.2", optional = true }
|
||||||
|
# TODO: Try remove on upgrade of `chrono-tz` crate.
|
||||||
|
regex = { version = "1.6", features = ["std"], default-features = false, optional = true }
|
||||||
|
# TODO: Try remove on upgrade of `bson` crate.
|
||||||
|
tap = { version = "1.0.1", optional = true }
|
||||||
|
# TODO: Remove on upgrade to 0.4.1 version of `graphql-parser`.
|
||||||
|
void = { version = "1.0.2", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
bencher = "0.1.2"
|
bencher = "0.1.2"
|
||||||
chrono = { version = "0.4.20", features = ["alloc"], default-features = false }
|
chrono = { version = "0.4.30", features = ["alloc"], default-features = false }
|
||||||
|
jiff = { version = "0.1.16", features = ["tzdb-bundle-always"], default-features = false }
|
||||||
pretty_assertions = "1.0.0"
|
pretty_assertions = "1.0.0"
|
||||||
serde_json = "1.0.2"
|
serde_json = "1.0.18"
|
||||||
|
serial_test = "3.0"
|
||||||
tokio = { version = "1.0", features = ["macros", "time", "rt-multi-thread"] }
|
tokio = { version = "1.0", features = ["macros", "time", "rt-multi-thread"] }
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "bench"
|
name = "bench"
|
||||||
harness = false
|
harness = false
|
||||||
path = "benches/bench.rs"
|
path = "benches/bench.rs"
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(nightly)'] }
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
BSD 2-Clause License
|
BSD 2-Clause License
|
||||||
|
|
||||||
Copyright (c) 2016-2022, Magnus Hallin
|
Copyright (c) 2016-2025 Magnus Hallin <mhallin@fastmail.com>,
|
||||||
|
Christoph Herzog <chris@theduke.at>,
|
||||||
|
Christian Legnitto <christian@legnitto.com>,
|
||||||
|
Ilya Solovyiov <ilya.solovyiov@gmail.com>,
|
||||||
|
Kai Ren <tyranron@gmail.com>
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
|
73
juniper/Makefile
Normal file
73
juniper/Makefile
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
###############################
|
||||||
|
# Common defaults/definitions #
|
||||||
|
###############################
|
||||||
|
|
||||||
|
# Checks two given strings for equality.
|
||||||
|
eq = $(if $(or $(1),$(2)),$(and $(findstring $(1),$(2)),\
|
||||||
|
$(findstring $(2),$(1))),1)
|
||||||
|
|
||||||
|
# Multiplatform prefix of `sed -i` commands.
|
||||||
|
sed-i = sed -i$(if $(call eq,$(shell uname -s),Darwin), '',)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
######################
|
||||||
|
# Project parameters #
|
||||||
|
######################
|
||||||
|
|
||||||
|
GRAPHIQL_VER ?= $(strip \
|
||||||
|
$(shell grep -m1 '"graphiql": "' package.json | cut -d '"' -f4))
|
||||||
|
GRAPHQL_PLAYGROUND_VER ?= $(strip \
|
||||||
|
$(shell grep -m1 '"graphql-playground-react": "' package.json | cut -d '"' -f4))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
############
|
||||||
|
# Commands #
|
||||||
|
############
|
||||||
|
|
||||||
|
# Download and prepare actual version of GraphiQL static files, used for
|
||||||
|
# integrating it.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# make graphiql
|
||||||
|
|
||||||
|
graphiql:
|
||||||
|
curl -fL -o src/http/graphiql.html \
|
||||||
|
https://raw.githubusercontent.com/graphql/graphiql/graphiql%40$(GRAPHIQL_VER)/examples/graphiql-cdn/index.html
|
||||||
|
$(sed-i) 's|unpkg.com/graphiql/|unpkg.com/graphiql@$(GRAPHIQL_VER)/|g' \
|
||||||
|
src/http/graphiql.html
|
||||||
|
$(sed-i) "s|'https://swapi-graphql.netlify.app/.netlify/functions/index'|JUNIPER_URL|g" \
|
||||||
|
src/http/graphiql.html
|
||||||
|
$(sed-i) "s|url: JUNIPER_URL,|url: JUNIPER_URL,\n subscriptionUrl: normalizeSubscriptionEndpoint(JUNIPER_URL, JUNIPER_SUBSCRIPTIONS_URL)|" \
|
||||||
|
src/http/graphiql.html
|
||||||
|
$(sed-i) 's|<script>|<script>\n<!-- inject -->|' \
|
||||||
|
src/http/graphiql.html
|
||||||
|
$(sed-i) '/X-Example-Header/d' \
|
||||||
|
src/http/graphiql.html
|
||||||
|
|
||||||
|
|
||||||
|
# Download and prepare actual version of GraphQL Playground static files, used
|
||||||
|
# for integrating it.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# make graphql-playground
|
||||||
|
|
||||||
|
graphql-playground:
|
||||||
|
curl -fL -o src/http/playground.html \
|
||||||
|
https://raw.githubusercontent.com/graphql/graphql-playground/graphql-playground-react%40$(GRAPHQL_PLAYGROUND_VER)/packages/graphql-playground-html/withAnimation.html
|
||||||
|
$(sed-i) 's|cdn.jsdelivr.net/npm/graphql-playground-react/|cdn.jsdelivr.net/npm/graphql-playground-react@$(GRAPHQL_PLAYGROUND_VER)/|g' \
|
||||||
|
src/http/playground.html
|
||||||
|
$(sed-i) "s|// you can add more options here|endpoint: 'JUNIPER_URL', subscriptionEndpoint: 'JUNIPER_SUBSCRIPTIONS_URL'|" \
|
||||||
|
src/http/playground.html
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##################
|
||||||
|
# .PHONY section #
|
||||||
|
##################
|
||||||
|
|
||||||
|
.PHONY: graphiql graphql-playground
|
|
@ -4,14 +4,15 @@ Juniper (GraphQL server library for Rust)
|
||||||
[![Crates.io](https://img.shields.io/crates/v/juniper.svg?maxAge=2592000)](https://crates.io/crates/juniper)
|
[![Crates.io](https://img.shields.io/crates/v/juniper.svg?maxAge=2592000)](https://crates.io/crates/juniper)
|
||||||
[![Documentation](https://docs.rs/juniper/badge.svg)](https://docs.rs/juniper)
|
[![Documentation](https://docs.rs/juniper/badge.svg)](https://docs.rs/juniper)
|
||||||
[![CI](https://github.com/graphql-rust/juniper/workflows/CI/badge.svg?branch=master "CI")](https://github.com/graphql-rust/juniper/actions?query=workflow%3ACI+branch%3Amaster)
|
[![CI](https://github.com/graphql-rust/juniper/workflows/CI/badge.svg?branch=master "CI")](https://github.com/graphql-rust/juniper/actions?query=workflow%3ACI+branch%3Amaster)
|
||||||
|
[![Rust 1.75+](https://img.shields.io/badge/rustc-1.75+-lightgray.svg "Rust 1.75+")](https://blog.rust-lang.org/2023/12/28/Rust-1.75.0.html)
|
||||||
|
|
||||||
- [Juniper Book] ([current][Juniper Book] | [edge][Juniper Book edge])
|
- [Juniper Book] ([current][Juniper Book] | [edge][Juniper Book edge])
|
||||||
- [Changelog](https://github.com/graphql-rust/juniper/blob/master/juniper/CHANGELOG.md)
|
- [Changelog](https://github.com/graphql-rust/juniper/blob/juniper-v0.16.1/juniper/CHANGELOG.md)
|
||||||
|
|
||||||
|
|
||||||
[GraphQL] is a data query language developed by [Facebook] and intended to serve mobile and web application frontends.
|
[GraphQL] is a data query language developed by [Facebook] and intended to serve mobile and web application frontends.
|
||||||
|
|
||||||
*[Juniper]* makes it possible to write [GraphQL] servers in [Rust] that are type-safe and blazingly fast. We also try to make declaring and resolving [GraphQL] schemas as convenient as possible as [Rust] will allow.
|
*[Juniper]* makes it possible to write [GraphQL] servers in [Rust] that are type-safe and blazingly fast. We also try to make declaring and resolving [GraphQL] schemas as convenient as [Rust] will allow.
|
||||||
|
|
||||||
[Juniper] doesn't include a web server - instead it provides building blocks to make integration with existing servers straightforward, including embedded [GraphiQL] and/or [GraphQL Playground] for easy debugging.
|
[Juniper] doesn't include a web server - instead it provides building blocks to make integration with existing servers straightforward, including embedded [GraphiQL] and/or [GraphQL Playground] for easy debugging.
|
||||||
|
|
||||||
|
@ -41,31 +42,31 @@ As an exception to other [GraphQL] libraries for other languages, [Juniper] buil
|
||||||
## Integrations
|
## Integrations
|
||||||
|
|
||||||
|
|
||||||
### Data types
|
### Types
|
||||||
|
|
||||||
[Juniper] has automatic integration with some very common [Rust] crates to make building schemas a breeze. The types from these crates will be usable in your schemas automatically:
|
[Juniper] provides out-of-the-box integration for some very common [Rust] crates to make building schemas a breeze. The types from these crates will be usable in your schemas automatically after enabling the correspondent self-titled [Cargo feature]:
|
||||||
- [`bigdecimal`] (feature gated)
|
- [`bigdecimal`]
|
||||||
- [`bson`]
|
- [`bson`]
|
||||||
- [`chrono`] (feature gated)
|
- [`chrono`], [`chrono-tz`]
|
||||||
- [`chrono-tz`] (feature gated)
|
- [`jiff`]
|
||||||
- [`rust_decimal`] (feature gated)
|
- [`rust_decimal`]
|
||||||
- [`time`] (feature gated)
|
- [`time`]
|
||||||
- [`url`]
|
- [`url`]
|
||||||
- [`uuid`]
|
- [`uuid`]
|
||||||
|
|
||||||
|
|
||||||
### Web servers
|
### Web server frameworks
|
||||||
|
|
||||||
- [`actix-web`] ([`juniper_actix`] crate)
|
- [`actix-web`] ([`juniper_actix`] crate)
|
||||||
|
- [`axum`] ([`juniper_axum`] crate)
|
||||||
- [`hyper`] ([`juniper_hyper`] crate)
|
- [`hyper`] ([`juniper_hyper`] crate)
|
||||||
- [`iron`] ([`juniper_iron`] crate)
|
|
||||||
- [`rocket`] ([`juniper_rocket`] crate)
|
- [`rocket`] ([`juniper_rocket`] crate)
|
||||||
- [`warp`] ([`juniper_warp`] crate)
|
- [`warp`] ([`juniper_warp`] crate)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## API Stability
|
## API stability
|
||||||
|
|
||||||
[Juniper] has not reached 1.0 yet, thus some API instability should be expected.
|
[Juniper] has not reached 1.0 yet, thus some API instability should be expected.
|
||||||
|
|
||||||
|
@ -74,36 +75,38 @@ As an exception to other [GraphQL] libraries for other languages, [Juniper] buil
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under [BSD 2-Clause License](https://github.com/graphql-rust/juniper/blob/master/juniper/LICENSE).
|
This project is licensed under [BSD 2-Clause License](https://github.com/graphql-rust/juniper/blob/juniper-v0.16.1/juniper/LICENSE).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[`actix-web`]: https://docs.rs/actix-web
|
[`actix-web`]: https://docs.rs/actix-web
|
||||||
|
[`axum`]: https://docs.rs/axum
|
||||||
[`bigdecimal`]: https://docs.rs/bigdecimal
|
[`bigdecimal`]: https://docs.rs/bigdecimal
|
||||||
[`bson`]: https://docs.rs/bson
|
[`bson`]: https://docs.rs/bson
|
||||||
[`chrono`]: https://docs.rs/chrono
|
[`chrono`]: https://docs.rs/chrono
|
||||||
[`chrono-tz`]: https://docs.rs/chrono-tz
|
[`chrono-tz`]: https://docs.rs/chrono-tz
|
||||||
|
[`jiff`]: https://docs.rs/jiff
|
||||||
[`juniper_actix`]: https://docs.rs/juniper_actix
|
[`juniper_actix`]: https://docs.rs/juniper_actix
|
||||||
|
[`juniper_axum`]: https://docs.rs/juniper_axum
|
||||||
[`juniper_hyper`]: https://docs.rs/juniper_hyper
|
[`juniper_hyper`]: https://docs.rs/juniper_hyper
|
||||||
[`juniper_iron`]: https://docs.rs/juniper_iron
|
|
||||||
[`juniper_rocket`]: https://docs.rs/juniper_rocket
|
[`juniper_rocket`]: https://docs.rs/juniper_rocket
|
||||||
[`juniper_warp`]: https://docs.rs/juniper_warp
|
[`juniper_warp`]: https://docs.rs/juniper_warp
|
||||||
[`hyper`]: https://docs.rs/hyper
|
[`hyper`]: https://docs.rs/hyper
|
||||||
[`iron`]: https://docs.rs/iron
|
|
||||||
[`rocket`]: https://docs.rs/rocket
|
[`rocket`]: https://docs.rs/rocket
|
||||||
[`rust_decimal`]: https://docs.rs/rust_decimal
|
[`rust_decimal`]: https://docs.rs/rust_decimal
|
||||||
[`time`]: https://docs.rs/time
|
[`time`]: https://docs.rs/time
|
||||||
[`url`]: https://docs.rs/url
|
[`url`]: https://docs.rs/url
|
||||||
[`uuid`]: https://docs.rs/uuid
|
[`uuid`]: https://docs.rs/uuid
|
||||||
[`warp`]: https://docs.rs/warp
|
[`warp`]: https://docs.rs/warp
|
||||||
|
[Cargo feature]: https://doc.rust-lang.org/cargo/reference/features.html
|
||||||
[Facebook]: https://facebook.com
|
[Facebook]: https://facebook.com
|
||||||
[GraphiQL]: https://github.com/graphql/graphiql
|
[GraphiQL]: https://github.com/graphql/graphiql
|
||||||
[GraphQL]: http://graphql.org
|
[GraphQL]: http://graphql.org
|
||||||
[GraphQL Playground]: https://github.com/graphql/graphql-playground
|
[GraphQL Playground]: https://github.com/graphql/graphql-playground
|
||||||
[Juniper]: https://docs.rs/juniper
|
[Juniper]: https://docs.rs/juniper
|
||||||
[Juniper Book]: https://graphql-rust.github.io
|
[Juniper Book]: https://graphql-rust.github.io/juniper
|
||||||
[Juniper Book edge]: https://graphql-rust.github.io/juniper/master
|
[Juniper Book edge]: https://graphql-rust.github.io/juniper/master
|
||||||
[Rust]: https://www.rust-lang.org
|
[Rust]: https://www.rust-lang.org
|
||||||
|
|
||||||
[1]: https://graphql-rust.github.io/quickstart.html
|
[1]: https://graphql-rust.github.io/juniper/quickstart.html
|
||||||
|
|
10
juniper/package.json
Normal file
10
juniper/package.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"postinstall": "make graphiql graphql-playground"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"graphiql": "3.8.3",
|
||||||
|
"graphql-playground-react": "1.7.28"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,45 +1,15 @@
|
||||||
[[pre-release-replacements]]
|
|
||||||
file = "../book/src/advanced/dataloaders.md"
|
|
||||||
exactly = 1
|
|
||||||
search = "juniper = \"[^\"]+\""
|
|
||||||
replace = "juniper = \"{{version}}\""
|
|
||||||
[[pre-release-replacements]]
|
|
||||||
file = "../book/src/advanced/subscriptions.md"
|
|
||||||
exactly = 1
|
|
||||||
search = "juniper = \"[^\"]+\""
|
|
||||||
replace = "juniper = \"{{version}}\""
|
|
||||||
[[pre-release-replacements]]
|
|
||||||
file = "../book/src/quickstart.md"
|
|
||||||
exactly = 1
|
|
||||||
search = "juniper = \"[^\"]+\""
|
|
||||||
replace = "juniper = \"{{version}}\""
|
|
||||||
[[pre-release-replacements]]
|
|
||||||
file = "../book/src/servers/hyper.md"
|
|
||||||
exactly = 1
|
|
||||||
search = "juniper = \"[^\"]+\""
|
|
||||||
replace = "juniper = \"{{version}}\""
|
|
||||||
[[pre-release-replacements]]
|
|
||||||
file = "../book/src/servers/iron.md"
|
|
||||||
exactly = 1
|
|
||||||
search = "juniper = \"[^\"]+\""
|
|
||||||
replace = "juniper = \"{{version}}\""
|
|
||||||
[[pre-release-replacements]]
|
|
||||||
file = "../book/src/servers/rocket.md"
|
|
||||||
exactly = 1
|
|
||||||
search = "juniper = \"[^\"]+\""
|
|
||||||
replace = "juniper = \"{{version}}\""
|
|
||||||
[[pre-release-replacements]]
|
|
||||||
file = "../book/src/servers/warp.md"
|
|
||||||
exactly = 1
|
|
||||||
search = "juniper = \"[^\"]+\""
|
|
||||||
replace = "juniper = \"{{version}}\""
|
|
||||||
|
|
||||||
[[pre-release-replacements]]
|
[[pre-release-replacements]]
|
||||||
file = "../juniper_actix/Cargo.toml"
|
file = "../juniper_actix/Cargo.toml"
|
||||||
exactly = 2
|
exactly = 2
|
||||||
search = "juniper = \\{ version = \"[^\"]+\""
|
search = "juniper = \\{ version = \"[^\"]+\""
|
||||||
replace = "juniper = { version = \"{{version}}\""
|
replace = "juniper = { version = \"{{version}}\""
|
||||||
|
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "../juniper_axum/Cargo.toml"
|
||||||
|
exactly = 2
|
||||||
|
search = "juniper = \\{ version = \"[^\"]+\""
|
||||||
|
replace = "juniper = { version = \"{{version}}\""
|
||||||
|
|
||||||
[[pre-release-replacements]]
|
[[pre-release-replacements]]
|
||||||
file = "../juniper_graphql_ws/Cargo.toml"
|
file = "../juniper_graphql_ws/Cargo.toml"
|
||||||
exactly = 1
|
exactly = 1
|
||||||
|
@ -52,12 +22,6 @@ exactly = 2
|
||||||
search = "juniper = \\{ version = \"[^\"]+\""
|
search = "juniper = \\{ version = \"[^\"]+\""
|
||||||
replace = "juniper = { version = \"{{version}}\""
|
replace = "juniper = { version = \"{{version}}\""
|
||||||
|
|
||||||
[[pre-release-replacements]]
|
|
||||||
file = "../juniper_iron/Cargo.toml"
|
|
||||||
exactly = 2
|
|
||||||
search = "juniper = \\{ version = \"[^\"]+\""
|
|
||||||
replace = "juniper = { version = \"{{version}}\""
|
|
||||||
|
|
||||||
[[pre-release-replacements]]
|
[[pre-release-replacements]]
|
||||||
file = "../juniper_rocket/Cargo.toml"
|
file = "../juniper_rocket/Cargo.toml"
|
||||||
exactly = 2
|
exactly = 2
|
||||||
|
@ -78,17 +42,95 @@ replace = "juniper = { version = \"{{version}}\""
|
||||||
|
|
||||||
[[pre-release-replacements]]
|
[[pre-release-replacements]]
|
||||||
file = "CHANGELOG.md"
|
file = "CHANGELOG.md"
|
||||||
exactly = 1
|
max = 1
|
||||||
|
min = 0
|
||||||
search = "## master"
|
search = "## master"
|
||||||
replace = "## [{{version}}] · {{date}}\n[{{version}}]: /../../tree/{{crate_name}}%40{{version}}/{{crate_name}}"
|
replace = "## [{{version}}] · {{date}}\n[{{version}}]: /../../tree/{{crate_name}}-v{{version}}/{{crate_name}}"
|
||||||
[[pre-release-replacements]]
|
[[pre-release-replacements]]
|
||||||
file = "CHANGELOG.md"
|
file = "CHANGELOG.md"
|
||||||
exactly = 1
|
max = 1
|
||||||
|
min = 0
|
||||||
search = "...master\\)"
|
search = "...master\\)"
|
||||||
replace = "...{{crate_name}}%40{{version}})"
|
replace = "...{{crate_name}}-v{{version}})"
|
||||||
|
|
||||||
[[pre-release-replacements]]
|
[[pre-release-replacements]]
|
||||||
file = "README.md"
|
file = "README.md"
|
||||||
exactly = 2
|
exactly = 2
|
||||||
search = "graphql-rust/juniper/blob/[^/]+/"
|
search = "graphql-rust/juniper/blob/[^/]+/"
|
||||||
replace = "graphql-rust/juniper/blob/{{crate_name}}%40{{version}}/"
|
replace = "graphql-rust/juniper/blob/{{crate_name}}-v{{version}}/"
|
||||||
|
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "../book/src/quickstart.md"
|
||||||
|
exactly = 1
|
||||||
|
search = "juniper = \"[^\"]+\""
|
||||||
|
replace = "juniper = \"{{version}}\""
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "../book/src/quickstart.md"
|
||||||
|
exactly = 1
|
||||||
|
search = "docs.rs/juniper/[^/]+/"
|
||||||
|
replace = "docs.rs/juniper/{{version}}/"
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "../book/src/advanced/implicit_and_explicit_null.md"
|
||||||
|
exactly = 1
|
||||||
|
search = "docs.rs/juniper/[^/]+/"
|
||||||
|
replace = "docs.rs/juniper/{{version}}/"
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "../book/src/advanced/lookahead.md"
|
||||||
|
exactly = 6
|
||||||
|
search = "docs.rs/juniper/[^/]+/"
|
||||||
|
replace = "docs.rs/juniper/{{version}}/"
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "../book/src/schema/index.md"
|
||||||
|
exactly = 5
|
||||||
|
search = "docs.rs/juniper/[^/]+/"
|
||||||
|
replace = "docs.rs/juniper/{{version}}/"
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "../book/src/schema/introspection.md"
|
||||||
|
exactly = 1
|
||||||
|
search = "docs.rs/juniper/[^/]+/"
|
||||||
|
replace = "docs.rs/juniper/{{version}}/"
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "../book/src/schema/subscriptions.md"
|
||||||
|
exactly = 3
|
||||||
|
search = "docs.rs/juniper/[^/]+/"
|
||||||
|
replace = "docs.rs/juniper/{{version}}/"
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "../book/src/types/enums.md"
|
||||||
|
exactly = 1
|
||||||
|
search = "docs.rs/juniper/[^/]+/"
|
||||||
|
replace = "docs.rs/juniper/{{version}}/"
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "../book/src/types/input_objects.md"
|
||||||
|
exactly = 1
|
||||||
|
search = "docs.rs/juniper/[^/]+/"
|
||||||
|
replace = "docs.rs/juniper/{{version}}/"
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "../book/src/types/interfaces.md"
|
||||||
|
exactly = 2
|
||||||
|
search = "docs.rs/juniper/[^/]+/"
|
||||||
|
replace = "docs.rs/juniper/{{version}}/"
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "../book/src/types/scalars.md"
|
||||||
|
exactly = 4
|
||||||
|
search = "docs.rs/juniper/[^/]+/"
|
||||||
|
replace = "docs.rs/juniper/{{version}}/"
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "../book/src/types/unions.md"
|
||||||
|
exactly = 1
|
||||||
|
search = "docs.rs/juniper/[^/]+/"
|
||||||
|
replace = "docs.rs/juniper/{{version}}/"
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "../book/src/types/objects/complex_fields.md"
|
||||||
|
exactly = 2
|
||||||
|
search = "docs.rs/juniper/[^/]+/"
|
||||||
|
replace = "docs.rs/juniper/{{version}}/"
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "../book/src/types/objects/index.md"
|
||||||
|
exactly = 3
|
||||||
|
search = "docs.rs/juniper/[^/]+/"
|
||||||
|
replace = "docs.rs/juniper/{{version}}/"
|
||||||
|
[[pre-release-replacements]]
|
||||||
|
file = "../book/src/types/objects/error/field.md"
|
||||||
|
exactly = 3
|
||||||
|
search = "docs.rs/juniper/[^/]+/"
|
||||||
|
replace = "docs.rs/juniper/{{version}}/"
|
||||||
|
|
|
@ -194,7 +194,7 @@ pub trait ToInputValue<S = DefaultScalarValue>: Sized {
|
||||||
fn to_input_value(&self) -> InputValue<S>;
|
fn to_input_value(&self) -> InputValue<S>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Type<'a> {
|
impl Type<'_> {
|
||||||
/// Get the name of a named type.
|
/// Get the name of a named type.
|
||||||
///
|
///
|
||||||
/// Only applies to named types; lists will return `None`.
|
/// Only applies to named types; lists will return `None`.
|
||||||
|
@ -221,7 +221,7 @@ impl<'a> Type<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> fmt::Display for Type<'a> {
|
impl fmt::Display for Type<'_> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Named(n) => write!(f, "{n}"),
|
Self::Named(n) => write!(f, "{n}"),
|
||||||
|
@ -567,8 +567,8 @@ impl<'a, S> Arguments<'a, S> {
|
||||||
pub fn get(&self, key: &str) -> Option<&Spanning<InputValue<S>>> {
|
pub fn get(&self, key: &str) -> Option<&Spanning<InputValue<S>>> {
|
||||||
self.items
|
self.items
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|&&(ref k, _)| k.item == key)
|
.filter(|&(k, _)| k.item == key)
|
||||||
.map(|&(_, ref v)| v)
|
.map(|(_, v)| v)
|
||||||
.next()
|
.next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -37,7 +37,7 @@ use crate::{
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
look_ahead::{
|
look_ahead::{
|
||||||
Applies, ChildSelection, ConcreteLookAheadSelection, LookAheadArgument, LookAheadMethods,
|
Applies, LookAheadArgument, LookAheadChildren, LookAheadList, LookAheadObject,
|
||||||
LookAheadSelection, LookAheadValue,
|
LookAheadSelection, LookAheadValue,
|
||||||
},
|
},
|
||||||
owned_executor::OwnedExecutor,
|
owned_executor::OwnedExecutor,
|
||||||
|
@ -87,7 +87,7 @@ where
|
||||||
///
|
///
|
||||||
/// All execution errors contain the source position in the query of the field
|
/// All execution errors contain the source position in the query of the field
|
||||||
/// that failed to resolve. It also contains the field stack.
|
/// that failed to resolve. It also contains the field stack.
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct ExecutionError<S> {
|
pub struct ExecutionError<S> {
|
||||||
location: SourcePosition,
|
location: SourcePosition,
|
||||||
path: Vec<String>,
|
path: Vec<String>,
|
||||||
|
@ -112,11 +112,7 @@ where
|
||||||
Self: PartialEq,
|
Self: PartialEq,
|
||||||
{
|
{
|
||||||
fn partial_cmp(&self, other: &ExecutionError<S>) -> Option<Ordering> {
|
fn partial_cmp(&self, other: &ExecutionError<S>) -> Option<Ordering> {
|
||||||
(&self.location, &self.path, &self.error.message).partial_cmp(&(
|
Some(self.cmp(other))
|
||||||
&other.location,
|
|
||||||
&other.path,
|
|
||||||
&other.error.message,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,7 +273,7 @@ impl<S> IntoFieldError<S> for std::convert::Infallible {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, S> IntoFieldError<S> for &'a str {
|
impl<S> IntoFieldError<S> for &str {
|
||||||
fn into_field_error(self) -> FieldError<S> {
|
fn into_field_error(self) -> FieldError<S> {
|
||||||
FieldError::<S>::from(self)
|
FieldError::<S>::from(self)
|
||||||
}
|
}
|
||||||
|
@ -289,7 +285,7 @@ impl<S> IntoFieldError<S> for String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, S> IntoFieldError<S> for Cow<'a, str> {
|
impl<S> IntoFieldError<S> for Cow<'_, str> {
|
||||||
fn into_field_error(self) -> FieldError<S> {
|
fn into_field_error(self) -> FieldError<S> {
|
||||||
FieldError::<S>::from(self)
|
FieldError::<S>::from(self)
|
||||||
}
|
}
|
||||||
|
@ -407,7 +403,7 @@ pub trait FromContext<T> {
|
||||||
/// Marker trait for types that can act as context objects for `GraphQL` types.
|
/// Marker trait for types that can act as context objects for `GraphQL` types.
|
||||||
pub trait Context {}
|
pub trait Context {}
|
||||||
|
|
||||||
impl<'a, C: Context> Context for &'a C {}
|
impl<C: Context> Context for &C {}
|
||||||
|
|
||||||
static NULL_CONTEXT: () = ();
|
static NULL_CONTEXT: () = ();
|
||||||
|
|
||||||
|
@ -630,7 +626,7 @@ where
|
||||||
/// Access the current context
|
/// Access the current context
|
||||||
///
|
///
|
||||||
/// You usually provide the context when calling the top-level `execute`
|
/// You usually provide the context when calling the top-level `execute`
|
||||||
/// function, or using the context factory in the Iron integration.
|
/// function, or using the context factory.
|
||||||
pub fn context(&self) -> &'r CtxT {
|
pub fn context(&self) -> &'r CtxT {
|
||||||
self.context
|
self.context
|
||||||
}
|
}
|
||||||
|
@ -702,47 +698,38 @@ where
|
||||||
};
|
};
|
||||||
self.parent_selection_set
|
self.parent_selection_set
|
||||||
.and_then(|p| {
|
.and_then(|p| {
|
||||||
// Search the parent's fields to find this field within the set
|
// Search the parent's fields to find this field within the selection set.
|
||||||
let found_field = p.iter().find(|&x| {
|
p.iter().find_map(|x| {
|
||||||
match *x {
|
match x {
|
||||||
Selection::Field(ref field) => {
|
Selection::Field(ref field) => {
|
||||||
let field = &field.item;
|
let field = &field.item;
|
||||||
// TODO: support excludes.
|
// TODO: support excludes.
|
||||||
let name = field.name.item;
|
let name = field.name.item;
|
||||||
let alias = field.alias.as_ref().map(|a| a.item);
|
let alias = field.alias.as_ref().map(|a| a.item);
|
||||||
alias.unwrap_or(name) == field_name
|
|
||||||
|
(alias.unwrap_or(name) == field_name).then(|| {
|
||||||
|
LookAheadSelection::new(
|
||||||
|
look_ahead::SelectionSource::Field(field),
|
||||||
|
self.variables,
|
||||||
|
self.fragments,
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
_ => false,
|
Selection::FragmentSpread(_) | Selection::InlineFragment(_) => None,
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
if let Some(p) = found_field {
|
|
||||||
LookAheadSelection::build_from_selection(p, self.variables, self.fragments)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
// We didn't find a field in the parent's selection matching
|
// We didn't find this field in the parent's selection matching it, which means
|
||||||
// this field, which means we're inside a FragmentSpread
|
// we're inside a `FragmentSpread`.
|
||||||
let mut ret = LookAheadSelection {
|
LookAheadSelection::new(
|
||||||
name: field_name,
|
look_ahead::SelectionSource::Spread {
|
||||||
alias: None,
|
field_name,
|
||||||
arguments: Vec::new(),
|
set: self.current_selection_set,
|
||||||
children: Vec::new(),
|
},
|
||||||
};
|
self.variables,
|
||||||
|
self.fragments,
|
||||||
// Add in all the children - this will mutate `ret`
|
)
|
||||||
if let Some(selection_set) = self.current_selection_set {
|
|
||||||
for c in selection_set {
|
|
||||||
LookAheadSelection::build_from_selection_with_parent(
|
|
||||||
c,
|
|
||||||
Some(&mut ret),
|
|
||||||
self.variables,
|
|
||||||
self.fragments,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ret
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -768,7 +755,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> FieldPath<'a> {
|
impl FieldPath<'_> {
|
||||||
fn construct_path(&self, acc: &mut Vec<String>) {
|
fn construct_path(&self, acc: &mut Vec<String>) {
|
||||||
match self {
|
match self {
|
||||||
FieldPath::Root(_) => (),
|
FieldPath::Root(_) => (),
|
||||||
|
@ -888,7 +875,7 @@ where
|
||||||
schema: &root_node.schema,
|
schema: &root_node.schema,
|
||||||
context,
|
context,
|
||||||
errors: &errors,
|
errors: &errors,
|
||||||
field_path: Arc::new(FieldPath::Root(operation.start)),
|
field_path: Arc::new(FieldPath::Root(operation.span.start)),
|
||||||
};
|
};
|
||||||
|
|
||||||
value = match operation.item.operation_type {
|
value = match operation.item.operation_type {
|
||||||
|
@ -940,7 +927,7 @@ where
|
||||||
defs.item
|
defs.item
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|&(ref name, ref def)| {
|
.filter_map(|(name, def)| {
|
||||||
def.default_value
|
def.default_value
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|i| (name.item.into(), i.item.clone()))
|
.map(|i| (name.item.into(), i.item.clone()))
|
||||||
|
@ -986,7 +973,7 @@ where
|
||||||
schema: &root_node.schema,
|
schema: &root_node.schema,
|
||||||
context,
|
context,
|
||||||
errors: &errors,
|
errors: &errors,
|
||||||
field_path: Arc::new(FieldPath::Root(operation.start)),
|
field_path: Arc::new(FieldPath::Root(operation.span.start)),
|
||||||
};
|
};
|
||||||
|
|
||||||
value = match operation.item.operation_type {
|
value = match operation.item.operation_type {
|
||||||
|
@ -1087,7 +1074,7 @@ where
|
||||||
defs.item
|
defs.item
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|&(ref name, ref def)| {
|
.filter_map(|(name, def)| {
|
||||||
def.default_value
|
def.default_value
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|i| (name.item.into(), i.item.clone()))
|
.map(|i| (name.item.into(), i.item.clone()))
|
||||||
|
@ -1132,7 +1119,7 @@ where
|
||||||
schema: &root_node.schema,
|
schema: &root_node.schema,
|
||||||
context,
|
context,
|
||||||
errors: &errors,
|
errors: &errors,
|
||||||
field_path: Arc::new(FieldPath::Root(operation.start)),
|
field_path: Arc::new(FieldPath::Root(operation.span.start)),
|
||||||
};
|
};
|
||||||
|
|
||||||
value = match operation.item.operation_type {
|
value = match operation.item.operation_type {
|
||||||
|
|
|
@ -25,7 +25,7 @@ pub struct OwnedExecutor<'a, CtxT, S> {
|
||||||
pub(super) field_path: Arc<FieldPath<'a>>,
|
pub(super) field_path: Arc<FieldPath<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, CtxT, S> Clone for OwnedExecutor<'a, CtxT, S>
|
impl<CtxT, S> Clone for OwnedExecutor<'_, CtxT, S>
|
||||||
where
|
where
|
||||||
S: Clone,
|
S: Clone,
|
||||||
{
|
{
|
||||||
|
|
|
@ -21,7 +21,7 @@ impl TestType {
|
||||||
|
|
||||||
async fn run_variable_query<F>(query: &str, vars: Variables<DefaultScalarValue>, f: F)
|
async fn run_variable_query<F>(query: &str, vars: Variables<DefaultScalarValue>, f: F)
|
||||||
where
|
where
|
||||||
F: Fn(&Object<DefaultScalarValue>) -> (),
|
F: Fn(&Object<DefaultScalarValue>),
|
||||||
{
|
{
|
||||||
let schema = RootNode::new(
|
let schema = RootNode::new(
|
||||||
TestType,
|
TestType,
|
||||||
|
@ -44,7 +44,7 @@ where
|
||||||
|
|
||||||
async fn run_query<F>(query: &str, f: F)
|
async fn run_query<F>(query: &str, f: F)
|
||||||
where
|
where
|
||||||
F: Fn(&Object<DefaultScalarValue>) -> (),
|
F: Fn(&Object<DefaultScalarValue>),
|
||||||
{
|
{
|
||||||
run_variable_query(query, Variables::new(), f).await;
|
run_variable_query(query, Variables::new(), f).await;
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ impl TestType {
|
||||||
|
|
||||||
async fn run_variable_query<F>(query: &str, vars: Variables<DefaultScalarValue>, f: F)
|
async fn run_variable_query<F>(query: &str, vars: Variables<DefaultScalarValue>, f: F)
|
||||||
where
|
where
|
||||||
F: Fn(&Object<DefaultScalarValue>) -> (),
|
F: Fn(&Object<DefaultScalarValue>),
|
||||||
{
|
{
|
||||||
let schema = RootNode::new(
|
let schema = RootNode::new(
|
||||||
TestType,
|
TestType,
|
||||||
|
@ -55,7 +55,7 @@ where
|
||||||
|
|
||||||
async fn run_query<F>(query: &str, f: F)
|
async fn run_query<F>(query: &str, f: F)
|
||||||
where
|
where
|
||||||
F: Fn(&Object<DefaultScalarValue>) -> (),
|
F: Fn(&Object<DefaultScalarValue>),
|
||||||
{
|
{
|
||||||
run_variable_query(query, Variables::new(), f).await;
|
run_variable_query(query, Variables::new(), f).await;
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,7 @@ async fn does_not_accept_string_literals() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
error,
|
error,
|
||||||
ValidationError(vec![RuleError::new(
|
ValidationError(vec![RuleError::new(
|
||||||
r#"Invalid value for argument "color", expected type "Color!""#,
|
r#"Invalid value for argument "color", reason: Invalid value ""RED"" for enum "Color""#,
|
||||||
&[SourcePosition::new(18, 0, 18)],
|
&[SourcePosition::new(18, 0, 18)],
|
||||||
)])
|
)])
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,6 +6,7 @@ mod interface {
|
||||||
GraphQLObject,
|
GraphQLObject,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[allow(dead_code)] // TODO: Consider this for the GraphQL interfaces in the expansion.
|
||||||
#[graphql_interface(for = [Cat, Dog])]
|
#[graphql_interface(for = [Cat, Dog])]
|
||||||
trait Pet {
|
trait Pet {
|
||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
|
|
|
@ -89,7 +89,7 @@ impl Root {
|
||||||
|
|
||||||
async fn run_type_info_query<F>(doc: &str, f: F)
|
async fn run_type_info_query<F>(doc: &str, f: F)
|
||||||
where
|
where
|
||||||
F: Fn((&Object<DefaultScalarValue>, &Vec<Value<DefaultScalarValue>>)) -> (),
|
F: Fn((&Object<DefaultScalarValue>, &Vec<Value<DefaultScalarValue>>)),
|
||||||
{
|
{
|
||||||
let schema = RootNode::new(
|
let schema = RootNode::new(
|
||||||
Root,
|
Root,
|
||||||
|
|
|
@ -114,7 +114,7 @@ impl Root {
|
||||||
|
|
||||||
async fn run_type_info_query<F>(doc: &str, f: F)
|
async fn run_type_info_query<F>(doc: &str, f: F)
|
||||||
where
|
where
|
||||||
F: Fn(&Object<DefaultScalarValue>, &Vec<Value<DefaultScalarValue>>) -> (),
|
F: Fn(&Object<DefaultScalarValue>, &Vec<Value<DefaultScalarValue>>),
|
||||||
{
|
{
|
||||||
let schema = RootNode::new(
|
let schema = RootNode::new(
|
||||||
Root,
|
Root,
|
||||||
|
|
|
@ -24,6 +24,7 @@ enum Sample {
|
||||||
struct Scalar(i32);
|
struct Scalar(i32);
|
||||||
|
|
||||||
/// A sample interface
|
/// A sample interface
|
||||||
|
#[allow(dead_code)] // TODO: Consider this for the GraphQL interfaces in the expansion.
|
||||||
#[graphql_interface(name = "SampleInterface", for = Root)]
|
#[graphql_interface(name = "SampleInterface", for = Root)]
|
||||||
trait Interface {
|
trait Interface {
|
||||||
/// A sample field in the interface
|
/// A sample field in the interface
|
||||||
|
|
|
@ -120,7 +120,7 @@ impl TestType {
|
||||||
|
|
||||||
async fn run_variable_query<F>(query: &str, vars: Variables<DefaultScalarValue>, f: F)
|
async fn run_variable_query<F>(query: &str, vars: Variables<DefaultScalarValue>, f: F)
|
||||||
where
|
where
|
||||||
F: Fn(&Object<DefaultScalarValue>) -> (),
|
F: Fn(&Object<DefaultScalarValue>),
|
||||||
{
|
{
|
||||||
let schema = RootNode::new(
|
let schema = RootNode::new(
|
||||||
TestType,
|
TestType,
|
||||||
|
@ -143,7 +143,7 @@ where
|
||||||
|
|
||||||
async fn run_query<F>(query: &str, f: F)
|
async fn run_query<F>(query: &str, f: F)
|
||||||
where
|
where
|
||||||
F: Fn(&Object<DefaultScalarValue>) -> (),
|
F: Fn(&Object<DefaultScalarValue>),
|
||||||
{
|
{
|
||||||
run_variable_query(query, graphql_vars! {}, f).await;
|
run_variable_query(query, graphql_vars! {}, f).await;
|
||||||
}
|
}
|
||||||
|
@ -916,7 +916,8 @@ async fn does_not_allow_missing_required_field() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
error,
|
error,
|
||||||
ValidationError(vec![RuleError::new(
|
ValidationError(vec![RuleError::new(
|
||||||
r#"Invalid value for argument "arg", expected type "ExampleInputObject!""#,
|
"Invalid value for argument \"arg\", \
|
||||||
|
reason: \"ExampleInputObject\" is missing fields: \"b\"",
|
||||||
&[SourcePosition::new(20, 0, 20)],
|
&[SourcePosition::new(20, 0, 20)],
|
||||||
)]),
|
)]),
|
||||||
);
|
);
|
||||||
|
@ -940,7 +941,9 @@ async fn does_not_allow_null_in_required_field() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
error,
|
error,
|
||||||
ValidationError(vec![RuleError::new(
|
ValidationError(vec![RuleError::new(
|
||||||
r#"Invalid value for argument "arg", expected type "ExampleInputObject!""#,
|
"Invalid value for argument \"arg\", \
|
||||||
|
reason: Error on \"ExampleInputObject\" field \"b\": \
|
||||||
|
\"null\" specified for not nullable type \"Int!\"",
|
||||||
&[SourcePosition::new(20, 0, 20)],
|
&[SourcePosition::new(20, 0, 20)],
|
||||||
)]),
|
)]),
|
||||||
);
|
);
|
||||||
|
|
82
juniper/src/http/graphiql.html
Normal file
82
juniper/src/http/graphiql.html
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<!--
|
||||||
|
* Copyright (c) 2021 GraphQL Contributors
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
-->
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>GraphiQL</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#graphiql {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!--
|
||||||
|
This GraphiQL example depends on Promise and fetch, which are available in
|
||||||
|
modern browsers, but can be "polyfilled" for older browsers.
|
||||||
|
GraphiQL itself depends on React DOM.
|
||||||
|
If you do not want to rely on a CDN, you can host these files locally or
|
||||||
|
include them directly in your favored resource bundler.
|
||||||
|
-->
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/react@18/umd/react.production.min.js"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
crossorigin
|
||||||
|
src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"
|
||||||
|
></script>
|
||||||
|
<!--
|
||||||
|
These two files can be found in the npm module, however you may wish to
|
||||||
|
copy them directly into your environment, or perhaps include them in your
|
||||||
|
favored resource bundler.
|
||||||
|
-->
|
||||||
|
<script
|
||||||
|
src="https://unpkg.com/graphiql@3.8.3/graphiql.min.js"
|
||||||
|
type="application/javascript"
|
||||||
|
></script>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/graphiql@3.8.3/graphiql.min.css" />
|
||||||
|
<!--
|
||||||
|
These are imports for the GraphIQL Explorer plugin.
|
||||||
|
-->
|
||||||
|
<script
|
||||||
|
src="https://unpkg.com/@graphiql/plugin-explorer/dist/index.umd.js"
|
||||||
|
crossorigin
|
||||||
|
></script>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://unpkg.com/@graphiql/plugin-explorer/dist/style.css"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="graphiql">Loading...</div>
|
||||||
|
<script>
|
||||||
|
<!-- inject -->
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('graphiql'));
|
||||||
|
const fetcher = GraphiQL.createFetcher({
|
||||||
|
url: JUNIPER_URL,
|
||||||
|
subscriptionUrl: normalizeSubscriptionEndpoint(JUNIPER_URL, JUNIPER_SUBSCRIPTIONS_URL)
|
||||||
|
});
|
||||||
|
const explorerPlugin = GraphiQLPluginExplorer.explorerPlugin();
|
||||||
|
root.render(
|
||||||
|
React.createElement(GraphiQL, {
|
||||||
|
fetcher,
|
||||||
|
defaultEditorToolsVisibility: true,
|
||||||
|
plugins: [explorerPlugin],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
14
juniper/src/http/graphiql.js
Normal file
14
juniper/src/http/graphiql.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
function normalizeSubscriptionEndpoint(endpoint, subscriptionEndpoint) {
|
||||||
|
if (subscriptionEndpoint) {
|
||||||
|
if (subscriptionEndpoint.startsWith('/')) {
|
||||||
|
const secure =
|
||||||
|
endpoint.includes('https') || location.href.includes('https')
|
||||||
|
? 's'
|
||||||
|
: ''
|
||||||
|
return `ws${secure}://${location.host}${subscriptionEndpoint}`
|
||||||
|
} else {
|
||||||
|
return subscriptionEndpoint.replace(/^http/, 'ws')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
|
@ -6,108 +6,26 @@
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use juniper::http::graphiql::graphiql_source;
|
/// # use juniper::http::graphiql::graphiql_source;
|
||||||
/// let graphiql = graphiql_source("/graphql", Some("ws://localhost:8080/subscriptions"));
|
/// let graphiql = graphiql_source("/graphql", Some("/subscriptions"));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn graphiql_source(
|
pub fn graphiql_source(
|
||||||
graphql_endpoint_url: &str,
|
graphql_endpoint_url: &str,
|
||||||
subscriptions_endpoint_url: Option<&str>,
|
subscriptions_endpoint_url: Option<&str>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let subscriptions_endpoint = if let Some(sub_url) = subscriptions_endpoint_url {
|
include_str!("graphiql.html").replace(
|
||||||
sub_url
|
"<!-- inject -->",
|
||||||
} else {
|
&format!(
|
||||||
""
|
// language=JavaScript
|
||||||
};
|
"
|
||||||
|
var JUNIPER_URL = '{juniper_url}';
|
||||||
|
var JUNIPER_SUBSCRIPTIONS_URL = '{juniper_subscriptions_url}';
|
||||||
|
|
||||||
let stylesheet_source = r#"
|
{grahiql_js}
|
||||||
<style>
|
|
||||||
html, body, #app {
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
"#;
|
|
||||||
let fetcher_source = r#"
|
|
||||||
<script>
|
|
||||||
if (usingSubscriptions) {
|
|
||||||
var subscriptionEndpoint = normalizeSubscriptionEndpoint(GRAPHQL_URL, GRAPHQL_SUBSCRIPTIONS_URL);
|
|
||||||
var subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(subscriptionEndpoint, { reconnect: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeSubscriptionEndpoint(endpoint, subscriptionEndpoint) {
|
",
|
||||||
if (subscriptionEndpoint) {
|
juniper_url = graphql_endpoint_url,
|
||||||
if (subscriptionEndpoint.startsWith('/')) {
|
juniper_subscriptions_url = subscriptions_endpoint_url.unwrap_or_default(),
|
||||||
const secure =
|
grahiql_js = include_str!("graphiql.js"),
|
||||||
endpoint.includes('https') || location.href.includes('https')
|
),
|
||||||
? 's'
|
|
||||||
: ''
|
|
||||||
return `ws${secure}://${location.host}${subscriptionEndpoint}`
|
|
||||||
} else {
|
|
||||||
return subscriptionEndpoint.replace(/^http/, 'ws')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function graphQLFetcher(params) {
|
|
||||||
return fetch(GRAPHQL_URL, {
|
|
||||||
method: 'post',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
body: JSON.stringify(params)
|
|
||||||
}).then(function (response) {
|
|
||||||
return response.text();
|
|
||||||
}).then(function (body) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(body);
|
|
||||||
} catch (error) {
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var fetcher = usingSubscriptions ? window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, graphQLFetcher) : graphQLFetcher;
|
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
React.createElement(GraphiQL, {
|
|
||||||
fetcher,
|
|
||||||
}),
|
|
||||||
document.querySelector('#app'));
|
|
||||||
</script>
|
|
||||||
"#;
|
|
||||||
|
|
||||||
format!(
|
|
||||||
r#"
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>GraphQL</title>
|
|
||||||
{stylesheet_source}
|
|
||||||
<link rel="stylesheet" type="text/css" href="//cdn.jsdelivr.net/npm/graphiql@0.17.5/graphiql.min.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.js"></script>
|
|
||||||
<script src="//unpkg.com/subscriptions-transport-ws@0.8.3/browser/client.js"></script>
|
|
||||||
<script src="//unpkg.com/graphiql-subscriptions-fetcher@0.0.2/browser/client.js"></script>
|
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
|
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
|
|
||||||
<script src="//cdn.jsdelivr.net/npm/graphiql@0.17.5/graphiql.min.js"></script>
|
|
||||||
<script>var GRAPHQL_URL = '{graphql_url}';</script>
|
|
||||||
<script>var usingSubscriptions = {using_subscriptions};</script>
|
|
||||||
<script>var GRAPHQL_SUBSCRIPTIONS_URL = '{graphql_subscriptions_url}';</script>
|
|
||||||
{fetcher_source}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"#,
|
|
||||||
graphql_url = graphql_endpoint_url,
|
|
||||||
stylesheet_source = stylesheet_source,
|
|
||||||
fetcher_source = fetcher_source,
|
|
||||||
graphql_subscriptions_url = subscriptions_endpoint,
|
|
||||||
using_subscriptions = subscriptions_endpoint_url.is_some(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ where
|
||||||
pub operation_name: Option<String>,
|
pub operation_name: Option<String>,
|
||||||
|
|
||||||
/// Optional variables to execute the GraphQL operation with.
|
/// Optional variables to execute the GraphQL operation with.
|
||||||
|
// TODO: Use `Variables` instead of `InputValue`?
|
||||||
#[serde(bound(
|
#[serde(bound(
|
||||||
deserialize = "InputValue<S>: Deserialize<'de>",
|
deserialize = "InputValue<S>: Deserialize<'de>",
|
||||||
serialize = "InputValue<S>: Serialize",
|
serialize = "InputValue<S>: Serialize",
|
||||||
|
@ -162,7 +163,7 @@ where
|
||||||
/// This struct implements Serialize, so you can simply serialize this
|
/// This struct implements Serialize, so you can simply serialize this
|
||||||
/// to JSON and send it over the wire. Use the `is_ok` method to determine
|
/// to JSON and send it over the wire. Use the `is_ok` method to determine
|
||||||
/// whether to send a 200 or 400 HTTP status code.
|
/// whether to send a 200 or 400 HTTP status code.
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct GraphQLResponse<S = DefaultScalarValue>(
|
pub struct GraphQLResponse<S = DefaultScalarValue>(
|
||||||
Result<(Value<S>, Vec<ExecutionError<S>>), GraphQLError>,
|
Result<(Value<S>, Vec<ExecutionError<S>>), GraphQLError>,
|
||||||
);
|
);
|
||||||
|
@ -238,11 +239,11 @@ where
|
||||||
/// A batch operation request.
|
/// A batch operation request.
|
||||||
///
|
///
|
||||||
/// Empty batch is considered as invalid value, so cannot be deserialized.
|
/// Empty batch is considered as invalid value, so cannot be deserialized.
|
||||||
#[serde(deserialize_with = "deserialize_non_empty_vec")]
|
#[serde(deserialize_with = "deserialize_non_empty_batch")]
|
||||||
Batch(Vec<GraphQLRequest<S>>),
|
Batch(Vec<GraphQLRequest<S>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_non_empty_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
|
fn deserialize_non_empty_batch<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
|
||||||
where
|
where
|
||||||
D: de::Deserializer<'de>,
|
D: de::Deserializer<'de>,
|
||||||
T: Deserialize<'de>,
|
T: Deserialize<'de>,
|
||||||
|
@ -251,7 +252,10 @@ where
|
||||||
|
|
||||||
let v = Vec::<T>::deserialize(deserializer)?;
|
let v = Vec::<T>::deserialize(deserializer)?;
|
||||||
if v.is_empty() {
|
if v.is_empty() {
|
||||||
Err(D::Error::invalid_length(0, &"a positive integer"))
|
Err(D::Error::invalid_length(
|
||||||
|
0,
|
||||||
|
&"non-empty batch of GraphQL requests",
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(v)
|
Ok(v)
|
||||||
}
|
}
|
||||||
|
@ -360,8 +364,11 @@ impl<S: ScalarValue> GraphQLBatchResponse<S> {
|
||||||
#[cfg(feature = "expose-test-schema")]
|
#[cfg(feature = "expose-test-schema")]
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use serde_json::Value as Json;
|
||||||
|
|
||||||
use crate::LocalBoxFuture;
|
use crate::LocalBoxFuture;
|
||||||
use serde_json::{self, Value as Json};
|
|
||||||
|
|
||||||
/// Normalized response content we expect to get back from
|
/// Normalized response content we expect to get back from
|
||||||
/// the http framework integration we are testing.
|
/// the http framework integration we are testing.
|
||||||
|
@ -400,6 +407,9 @@ pub mod tests {
|
||||||
println!(" - test_get_with_variables");
|
println!(" - test_get_with_variables");
|
||||||
test_get_with_variables(integration);
|
test_get_with_variables(integration);
|
||||||
|
|
||||||
|
println!(" - test_post_with_variables");
|
||||||
|
test_post_with_variables(integration);
|
||||||
|
|
||||||
println!(" - test_simple_post");
|
println!(" - test_simple_post");
|
||||||
test_simple_post(integration);
|
test_simple_post(integration);
|
||||||
|
|
||||||
|
@ -498,13 +508,48 @@ pub mod tests {
|
||||||
"NEW_HOPE",
|
"NEW_HOPE",
|
||||||
"EMPIRE",
|
"EMPIRE",
|
||||||
"JEDI"
|
"JEDI"
|
||||||
],
|
],
|
||||||
"homePlanet": "Tatooine",
|
"homePlanet": "Tatooine",
|
||||||
"name": "Luke Skywalker",
|
"name": "Luke Skywalker",
|
||||||
"id": "1000"
|
"id": "1000"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}"#
|
}
|
||||||
|
}"#
|
||||||
|
)
|
||||||
|
.expect("Invalid JSON constant in test")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_post_with_variables<T: HttpIntegration>(integration: &T) {
|
||||||
|
let response = integration.post_json(
|
||||||
|
"/",
|
||||||
|
r#"{
|
||||||
|
"query":
|
||||||
|
"query($id: String!) { human(id: $id) { id, name, appearsIn, homePlanet } }",
|
||||||
|
"variables": {"id": "1000"}
|
||||||
|
}"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(response.status_code, 200);
|
||||||
|
assert_eq!(response.content_type, "application/json");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
unwrap_json_response(&response),
|
||||||
|
serde_json::from_str::<Json>(
|
||||||
|
r#"{
|
||||||
|
"data": {
|
||||||
|
"human": {
|
||||||
|
"appearsIn": [
|
||||||
|
"NEW_HOPE",
|
||||||
|
"EMPIRE",
|
||||||
|
"JEDI"
|
||||||
|
],
|
||||||
|
"homePlanet": "Tatooine",
|
||||||
|
"name": "Luke Skywalker",
|
||||||
|
"id": "1000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#
|
||||||
)
|
)
|
||||||
.expect("Invalid JSON constant in test")
|
.expect("Invalid JSON constant in test")
|
||||||
);
|
);
|
||||||
|
@ -598,162 +643,281 @@ pub mod tests {
|
||||||
) -> LocalBoxFuture<Result<(), anyhow::Error>>;
|
) -> LocalBoxFuture<Result<(), anyhow::Error>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// WebSocket framework integration message
|
/// WebSocket framework integration message.
|
||||||
pub enum WsIntegrationMessage {
|
pub enum WsIntegrationMessage {
|
||||||
/// Send message through the WebSocket
|
/// Send a message through a WebSocket.
|
||||||
/// Takes a message as a String
|
Send(Json),
|
||||||
Send(String),
|
|
||||||
/// Expect message to come through the WebSocket
|
/// Expects a message to come through a WebSocket, with the specified timeout.
|
||||||
/// Takes expected message as a String and a timeout in milliseconds
|
Expect(Json, Duration),
|
||||||
Expect(String, u64),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default value in milliseconds for how long to wait for an incoming message
|
/// Default value in milliseconds for how long to wait for an incoming WebSocket message.
|
||||||
pub const WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT: u64 = 100;
|
pub const WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT: Duration = Duration::from_millis(100);
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
/// Integration tests for the [legacy `graphql-ws` GraphQL over WebSocket Protocol][old].
|
||||||
pub async fn run_ws_test_suite<T: WsIntegration>(integration: &T) {
|
///
|
||||||
println!("Running WebSocket Test suite for integration");
|
/// [old]: https://github.com/apollographql/subscriptions-transport-ws/blob/v0.11.0/PROTOCOL.md
|
||||||
|
pub mod graphql_ws {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
println!(" - test_ws_simple_subscription");
|
use super::{WsIntegration, WsIntegrationMessage, WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT};
|
||||||
test_ws_simple_subscription(integration).await;
|
|
||||||
|
|
||||||
println!(" - test_ws_invalid_json");
|
#[allow(missing_docs)]
|
||||||
test_ws_invalid_json(integration).await;
|
pub async fn run_test_suite<T: WsIntegration>(integration: &T) {
|
||||||
|
println!("Running `graphql-ws` test suite for integration");
|
||||||
|
|
||||||
println!(" - test_ws_invalid_query");
|
println!(" - graphql_ws::test_simple_subscription");
|
||||||
test_ws_invalid_query(integration).await;
|
test_simple_subscription(integration).await;
|
||||||
|
|
||||||
|
println!(" - graphql_ws::test_invalid_json");
|
||||||
|
test_invalid_json(integration).await;
|
||||||
|
|
||||||
|
println!(" - graphql_ws::test_invalid_query");
|
||||||
|
test_invalid_query(integration).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_simple_subscription<T: WsIntegration>(integration: &T) {
|
||||||
|
let messages = vec![
|
||||||
|
WsIntegrationMessage::Send(json!({
|
||||||
|
"type": "connection_init",
|
||||||
|
"payload": {},
|
||||||
|
})),
|
||||||
|
WsIntegrationMessage::Expect(
|
||||||
|
json!({"type": "connection_ack"}),
|
||||||
|
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
||||||
|
),
|
||||||
|
WsIntegrationMessage::Expect(
|
||||||
|
json!({"type": "ka"}),
|
||||||
|
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
||||||
|
),
|
||||||
|
WsIntegrationMessage::Send(json!({
|
||||||
|
"id": "1",
|
||||||
|
"type": "start",
|
||||||
|
"payload": {
|
||||||
|
"variables": {},
|
||||||
|
"extensions": {},
|
||||||
|
"operationName": null,
|
||||||
|
"query": "subscription { asyncHuman { id, name, homePlanet } }",
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
WsIntegrationMessage::Expect(
|
||||||
|
json!({
|
||||||
|
"type": "data",
|
||||||
|
"id": "1",
|
||||||
|
"payload": {
|
||||||
|
"data": {
|
||||||
|
"asyncHuman": {
|
||||||
|
"id": "1000",
|
||||||
|
"name": "Luke Skywalker",
|
||||||
|
"homePlanet": "Tatooine",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
integration.run(messages).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_invalid_json<T: WsIntegration>(integration: &T) {
|
||||||
|
let messages = vec![
|
||||||
|
WsIntegrationMessage::Send(json!({"whatever": "invalid value"})),
|
||||||
|
WsIntegrationMessage::Expect(
|
||||||
|
json!({
|
||||||
|
"type": "connection_error",
|
||||||
|
"payload": {
|
||||||
|
"message": "`serde` error: missing field `type` at line 1 column 28",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
integration.run(messages).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_invalid_query<T: WsIntegration>(integration: &T) {
|
||||||
|
let messages = vec![
|
||||||
|
WsIntegrationMessage::Send(json!({
|
||||||
|
"type": "connection_init",
|
||||||
|
"payload": {},
|
||||||
|
})),
|
||||||
|
WsIntegrationMessage::Expect(
|
||||||
|
json!({"type": "connection_ack"}),
|
||||||
|
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
||||||
|
),
|
||||||
|
WsIntegrationMessage::Expect(
|
||||||
|
json!({"type": "ka"}),
|
||||||
|
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
||||||
|
),
|
||||||
|
WsIntegrationMessage::Send(json!({
|
||||||
|
"id": "1",
|
||||||
|
"type": "start",
|
||||||
|
"payload": {
|
||||||
|
"variables": {},
|
||||||
|
"extensions": {},
|
||||||
|
"operationName": null,
|
||||||
|
"query": "subscription { asyncHuman }",
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
WsIntegrationMessage::Expect(
|
||||||
|
json!({
|
||||||
|
"type": "error",
|
||||||
|
"id": "1",
|
||||||
|
"payload": [{
|
||||||
|
"message": "Field \"asyncHuman\" of type \"Human!\" must have a selection \
|
||||||
|
of subfields. Did you mean \"asyncHuman { ... }\"?",
|
||||||
|
"locations": [{
|
||||||
|
"line": 1,
|
||||||
|
"column": 16,
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
integration.run(messages).await.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn test_ws_simple_subscription<T: WsIntegration>(integration: &T) {
|
/// Integration tests for the [new `graphql-transport-ws` GraphQL over WebSocket Protocol][new].
|
||||||
let messages = vec![
|
///
|
||||||
WsIntegrationMessage::Send(
|
/// [new]: https://github.com/enisdenjo/graphql-ws/blob/v5.14.0/PROTOCOL.md
|
||||||
r#"{
|
pub mod graphql_transport_ws {
|
||||||
"type":"connection_init",
|
use serde_json::json;
|
||||||
"payload":{}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
WsIntegrationMessage::Expect(
|
|
||||||
r#"{
|
|
||||||
"type":"connection_ack"
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
|
||||||
),
|
|
||||||
WsIntegrationMessage::Expect(
|
|
||||||
r#"{
|
|
||||||
"type":"ka"
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
|
||||||
),
|
|
||||||
WsIntegrationMessage::Send(
|
|
||||||
r#"{
|
|
||||||
"id":"1",
|
|
||||||
"type":"start",
|
|
||||||
"payload":{
|
|
||||||
"variables":{},
|
|
||||||
"extensions":{},
|
|
||||||
"operationName":null,
|
|
||||||
"query":"subscription { asyncHuman { id, name, homePlanet } }"
|
|
||||||
}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
WsIntegrationMessage::Expect(
|
|
||||||
r#"{
|
|
||||||
"type":"data",
|
|
||||||
"id":"1",
|
|
||||||
"payload":{
|
|
||||||
"data":{
|
|
||||||
"asyncHuman":{
|
|
||||||
"id":"1000",
|
|
||||||
"name":"Luke Skywalker",
|
|
||||||
"homePlanet":"Tatooine"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
integration.run(messages).await.unwrap();
|
use super::{WsIntegration, WsIntegrationMessage, WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT};
|
||||||
}
|
|
||||||
|
|
||||||
async fn test_ws_invalid_json<T: WsIntegration>(integration: &T) {
|
#[allow(missing_docs)]
|
||||||
let messages = vec![
|
pub async fn run_test_suite<T: WsIntegration>(integration: &T) {
|
||||||
WsIntegrationMessage::Send("invalid json".into()),
|
println!("Running `graphql-transport-ws` test suite for integration");
|
||||||
WsIntegrationMessage::Expect(
|
|
||||||
r#"{
|
|
||||||
"type":"connection_error",
|
|
||||||
"payload":{
|
|
||||||
"message":"serde error: expected value at line 1 column 1"
|
|
||||||
}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
integration.run(messages).await.unwrap();
|
println!(" - graphql_ws::test_simple_subscription");
|
||||||
}
|
test_simple_subscription(integration).await;
|
||||||
|
|
||||||
async fn test_ws_invalid_query<T: WsIntegration>(integration: &T) {
|
println!(" - graphql_ws::test_invalid_json");
|
||||||
let messages = vec![
|
test_invalid_json(integration).await;
|
||||||
WsIntegrationMessage::Send(
|
|
||||||
r#"{
|
|
||||||
"type":"connection_init",
|
|
||||||
"payload":{}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
WsIntegrationMessage::Expect(
|
|
||||||
r#"{
|
|
||||||
"type":"connection_ack"
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT
|
|
||||||
),
|
|
||||||
WsIntegrationMessage::Expect(
|
|
||||||
r#"{
|
|
||||||
"type":"ka"
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT
|
|
||||||
),
|
|
||||||
WsIntegrationMessage::Send(
|
|
||||||
r#"{
|
|
||||||
"id":"1",
|
|
||||||
"type":"start",
|
|
||||||
"payload":{
|
|
||||||
"variables":{},
|
|
||||||
"extensions":{},
|
|
||||||
"operationName":null,
|
|
||||||
"query":"subscription { asyncHuman }"
|
|
||||||
}
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
WsIntegrationMessage::Expect(
|
|
||||||
r#"{
|
|
||||||
"type":"error",
|
|
||||||
"id":"1",
|
|
||||||
"payload":[{
|
|
||||||
"message":"Field \"asyncHuman\" of type \"Human!\" must have a selection of subfields. Did you mean \"asyncHuman { ... }\"?",
|
|
||||||
"locations":[{
|
|
||||||
"line":1,
|
|
||||||
"column":16
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
}"#
|
|
||||||
.into(),
|
|
||||||
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT
|
|
||||||
)
|
|
||||||
];
|
|
||||||
|
|
||||||
integration.run(messages).await.unwrap();
|
println!(" - graphql_ws::test_invalid_query");
|
||||||
|
test_invalid_query(integration).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_simple_subscription<T: WsIntegration>(integration: &T) {
|
||||||
|
let messages = vec![
|
||||||
|
WsIntegrationMessage::Send(json!({
|
||||||
|
"type": "connection_init",
|
||||||
|
"payload": {},
|
||||||
|
})),
|
||||||
|
WsIntegrationMessage::Expect(
|
||||||
|
json!({"type": "connection_ack"}),
|
||||||
|
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
||||||
|
),
|
||||||
|
WsIntegrationMessage::Expect(
|
||||||
|
json!({"type": "pong"}),
|
||||||
|
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
||||||
|
),
|
||||||
|
WsIntegrationMessage::Send(json!({"type": "ping"})),
|
||||||
|
WsIntegrationMessage::Expect(
|
||||||
|
json!({"type": "pong"}),
|
||||||
|
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
||||||
|
),
|
||||||
|
WsIntegrationMessage::Send(json!({
|
||||||
|
"id": "1",
|
||||||
|
"type": "subscribe",
|
||||||
|
"payload": {
|
||||||
|
"variables": {},
|
||||||
|
"extensions": {},
|
||||||
|
"operationName": null,
|
||||||
|
"query": "subscription { asyncHuman { id, name, homePlanet } }",
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
WsIntegrationMessage::Expect(
|
||||||
|
json!({
|
||||||
|
"id": "1",
|
||||||
|
"type": "next",
|
||||||
|
"payload": {
|
||||||
|
"data": {
|
||||||
|
"asyncHuman": {
|
||||||
|
"id": "1000",
|
||||||
|
"name": "Luke Skywalker",
|
||||||
|
"homePlanet": "Tatooine",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
integration.run(messages).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_invalid_json<T: WsIntegration>(integration: &T) {
|
||||||
|
let messages = vec![
|
||||||
|
WsIntegrationMessage::Send(json!({"whatever": "invalid value"})),
|
||||||
|
WsIntegrationMessage::Expect(
|
||||||
|
json!({
|
||||||
|
"code": 4400,
|
||||||
|
"description": "`serde` error: missing field `type` at line 1 column 28",
|
||||||
|
}),
|
||||||
|
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
integration.run(messages).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_invalid_query<T: WsIntegration>(integration: &T) {
|
||||||
|
let messages = vec![
|
||||||
|
WsIntegrationMessage::Send(json!({
|
||||||
|
"type": "connection_init",
|
||||||
|
"payload": {},
|
||||||
|
})),
|
||||||
|
WsIntegrationMessage::Expect(
|
||||||
|
json!({"type": "connection_ack"}),
|
||||||
|
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
||||||
|
),
|
||||||
|
WsIntegrationMessage::Expect(
|
||||||
|
json!({"type": "pong"}),
|
||||||
|
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
||||||
|
),
|
||||||
|
WsIntegrationMessage::Send(json!({"type": "ping"})),
|
||||||
|
WsIntegrationMessage::Expect(
|
||||||
|
json!({"type": "pong"}),
|
||||||
|
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
||||||
|
),
|
||||||
|
WsIntegrationMessage::Send(json!({
|
||||||
|
"id": "1",
|
||||||
|
"type": "subscribe",
|
||||||
|
"payload": {
|
||||||
|
"variables": {},
|
||||||
|
"extensions": {},
|
||||||
|
"operationName": null,
|
||||||
|
"query": "subscription { asyncHuman }",
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
WsIntegrationMessage::Expect(
|
||||||
|
json!({
|
||||||
|
"type": "error",
|
||||||
|
"id": "1",
|
||||||
|
"payload": [{
|
||||||
|
"message": "Field \"asyncHuman\" of type \"Human!\" must have a selection \
|
||||||
|
of subfields. Did you mean \"asyncHuman { ... }\"?",
|
||||||
|
"locations": [{
|
||||||
|
"line": 1,
|
||||||
|
"column": 16,
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
WS_INTEGRATION_EXPECT_DEFAULT_TIMEOUT,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
integration.run(messages).await.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
540
juniper/src/http/playground.html
Normal file
540
juniper/src/http/playground.html
Normal file
|
@ -0,0 +1,540 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset=utf-8 />
|
||||||
|
<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
|
||||||
|
<title>GraphQL Playground</title>
|
||||||
|
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/graphql-playground-react@1.7.28/build/static/css/index.css" />
|
||||||
|
<link rel="shortcut icon" href="//cdn.jsdelivr.net/npm/graphql-playground-react@1.7.28/build/favicon.png" />
|
||||||
|
<script src="//cdn.jsdelivr.net/npm/graphql-playground-react@1.7.28/build/static/js/middleware.js"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<style type="text/css">
|
||||||
|
html {
|
||||||
|
font-family: "Open Sans", sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #172a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playgroundIn {
|
||||||
|
-webkit-animation: playgroundIn 0.5s ease-out forwards;
|
||||||
|
animation: playgroundIn 0.5s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes playgroundIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(10px);
|
||||||
|
-ms-transform: translateY(10px);
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0);
|
||||||
|
-ms-transform: translateY(0);
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes playgroundIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(10px);
|
||||||
|
-ms-transform: translateY(10px);
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0);
|
||||||
|
-ms-transform: translateY(0);
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
.fadeOut {
|
||||||
|
-webkit-animation: fadeOut 0.5s ease-out forwards;
|
||||||
|
animation: fadeOut 0.5s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(-10px);
|
||||||
|
-ms-transform: translateY(-10px);
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0);
|
||||||
|
-ms-transform: translateY(0);
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(-10px);
|
||||||
|
-ms-transform: translateY(-10px);
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0);
|
||||||
|
-ms-transform: translateY(0);
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes fadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0);
|
||||||
|
-ms-transform: translateY(0);
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(-10px);
|
||||||
|
-ms-transform: translateY(-10px);
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeOut {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0);
|
||||||
|
-ms-transform: translateY(0);
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(-10px);
|
||||||
|
-ms-transform: translateY(-10px);
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes appearIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(0px);
|
||||||
|
-ms-transform: translateY(0px);
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0);
|
||||||
|
-ms-transform: translateY(0);
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes appearIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: translateY(0px);
|
||||||
|
-ms-transform: translateY(0px);
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: translateY(0);
|
||||||
|
-ms-transform: translateY(0);
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes scaleIn {
|
||||||
|
from {
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-ms-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
-ms-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scaleIn {
|
||||||
|
from {
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-ms-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
-ms-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes innerDrawIn {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: 70;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
stroke-dashoffset: 140;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 210;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes innerDrawIn {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: 70;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
stroke-dashoffset: 140;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 210;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes outerDrawIn {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: 76;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 152;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes outerDrawIn {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: 76;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: 152;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hHWjkv {
|
||||||
|
-webkit-transform-origin: 0px 0px;
|
||||||
|
-ms-transform-origin: 0px 0px;
|
||||||
|
transform-origin: 0px 0px;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-ms-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
-webkit-animation: scaleIn 0.25s linear forwards 0.2222222222222222s;
|
||||||
|
animation: scaleIn 0.25s linear forwards 0.2222222222222222s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gCDOzd {
|
||||||
|
-webkit-transform-origin: 0px 0px;
|
||||||
|
-ms-transform-origin: 0px 0px;
|
||||||
|
transform-origin: 0px 0px;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-ms-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
-webkit-animation: scaleIn 0.25s linear forwards 0.4222222222222222s;
|
||||||
|
animation: scaleIn 0.25s linear forwards 0.4222222222222222s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hmCcxi {
|
||||||
|
-webkit-transform-origin: 0px 0px;
|
||||||
|
-ms-transform-origin: 0px 0px;
|
||||||
|
transform-origin: 0px 0px;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-ms-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
-webkit-animation: scaleIn 0.25s linear forwards 0.6222222222222222s;
|
||||||
|
animation: scaleIn 0.25s linear forwards 0.6222222222222222s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eHamQi {
|
||||||
|
-webkit-transform-origin: 0px 0px;
|
||||||
|
-ms-transform-origin: 0px 0px;
|
||||||
|
transform-origin: 0px 0px;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-ms-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
-webkit-animation: scaleIn 0.25s linear forwards 0.8222222222222223s;
|
||||||
|
animation: scaleIn 0.25s linear forwards 0.8222222222222223s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.byhgGu {
|
||||||
|
-webkit-transform-origin: 0px 0px;
|
||||||
|
-ms-transform-origin: 0px 0px;
|
||||||
|
transform-origin: 0px 0px;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-ms-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
-webkit-animation: scaleIn 0.25s linear forwards 1.0222222222222221s;
|
||||||
|
animation: scaleIn 0.25s linear forwards 1.0222222222222221s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.llAKP {
|
||||||
|
-webkit-transform-origin: 0px 0px;
|
||||||
|
-ms-transform-origin: 0px 0px;
|
||||||
|
transform-origin: 0px 0px;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-ms-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
-webkit-animation: scaleIn 0.25s linear forwards 1.2222222222222223s;
|
||||||
|
animation: scaleIn 0.25s linear forwards 1.2222222222222223s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bglIGM {
|
||||||
|
-webkit-transform-origin: 64px 28px;
|
||||||
|
-ms-transform-origin: 64px 28px;
|
||||||
|
transform-origin: 64px 28px;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-ms-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
-webkit-animation: scaleIn 0.25s linear forwards 0.2222222222222222s;
|
||||||
|
animation: scaleIn 0.25s linear forwards 0.2222222222222222s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ksxRII {
|
||||||
|
-webkit-transform-origin: 95.98500061035156px 46.510000228881836px;
|
||||||
|
-ms-transform-origin: 95.98500061035156px 46.510000228881836px;
|
||||||
|
transform-origin: 95.98500061035156px 46.510000228881836px;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-ms-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
-webkit-animation: scaleIn 0.25s linear forwards 0.4222222222222222s;
|
||||||
|
animation: scaleIn 0.25s linear forwards 0.4222222222222222s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cWrBmb {
|
||||||
|
-webkit-transform-origin: 95.97162628173828px 83.4900016784668px;
|
||||||
|
-ms-transform-origin: 95.97162628173828px 83.4900016784668px;
|
||||||
|
transform-origin: 95.97162628173828px 83.4900016784668px;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-ms-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
-webkit-animation: scaleIn 0.25s linear forwards 0.6222222222222222s;
|
||||||
|
animation: scaleIn 0.25s linear forwards 0.6222222222222222s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Wnusb {
|
||||||
|
-webkit-transform-origin: 64px 101.97999572753906px;
|
||||||
|
-ms-transform-origin: 64px 101.97999572753906px;
|
||||||
|
transform-origin: 64px 101.97999572753906px;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-ms-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
-webkit-animation: scaleIn 0.25s linear forwards 0.8222222222222223s;
|
||||||
|
animation: scaleIn 0.25s linear forwards 0.8222222222222223s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bfPqf {
|
||||||
|
-webkit-transform-origin: 32.03982162475586px 83.4900016784668px;
|
||||||
|
-ms-transform-origin: 32.03982162475586px 83.4900016784668px;
|
||||||
|
transform-origin: 32.03982162475586px 83.4900016784668px;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-ms-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
-webkit-animation: scaleIn 0.25s linear forwards 1.0222222222222221s;
|
||||||
|
animation: scaleIn 0.25s linear forwards 1.0222222222222221s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edRCTN {
|
||||||
|
-webkit-transform-origin: 32.033552169799805px 46.510000228881836px;
|
||||||
|
-ms-transform-origin: 32.033552169799805px 46.510000228881836px;
|
||||||
|
transform-origin: 32.033552169799805px 46.510000228881836px;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-ms-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
-webkit-animation: scaleIn 0.25s linear forwards 1.2222222222222223s;
|
||||||
|
animation: scaleIn 0.25s linear forwards 1.2222222222222223s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iEGVWn {
|
||||||
|
opacity: 0;
|
||||||
|
stroke-dasharray: 76;
|
||||||
|
-webkit-animation: outerDrawIn 0.5s ease-out forwards 0.3333333333333333s, appearIn 0.1s ease-out forwards 0.3333333333333333s;
|
||||||
|
animation: outerDrawIn 0.5s ease-out forwards 0.3333333333333333s, appearIn 0.1s ease-out forwards 0.3333333333333333s;
|
||||||
|
-webkit-animation-iteration-count: 1, 1;
|
||||||
|
animation-iteration-count: 1, 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsocdx {
|
||||||
|
opacity: 0;
|
||||||
|
stroke-dasharray: 76;
|
||||||
|
-webkit-animation: outerDrawIn 0.5s ease-out forwards 0.5333333333333333s, appearIn 0.1s ease-out forwards 0.5333333333333333s;
|
||||||
|
animation: outerDrawIn 0.5s ease-out forwards 0.5333333333333333s, appearIn 0.1s ease-out forwards 0.5333333333333333s;
|
||||||
|
-webkit-animation-iteration-count: 1, 1;
|
||||||
|
animation-iteration-count: 1, 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jAZXmP {
|
||||||
|
opacity: 0;
|
||||||
|
stroke-dasharray: 76;
|
||||||
|
-webkit-animation: outerDrawIn 0.5s ease-out forwards 0.7333333333333334s, appearIn 0.1s ease-out forwards 0.7333333333333334s;
|
||||||
|
animation: outerDrawIn 0.5s ease-out forwards 0.7333333333333334s, appearIn 0.1s ease-out forwards 0.7333333333333334s;
|
||||||
|
-webkit-animation-iteration-count: 1, 1;
|
||||||
|
animation-iteration-count: 1, 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hSeArx {
|
||||||
|
opacity: 0;
|
||||||
|
stroke-dasharray: 76;
|
||||||
|
-webkit-animation: outerDrawIn 0.5s ease-out forwards 0.9333333333333333s, appearIn 0.1s ease-out forwards 0.9333333333333333s;
|
||||||
|
animation: outerDrawIn 0.5s ease-out forwards 0.9333333333333333s, appearIn 0.1s ease-out forwards 0.9333333333333333s;
|
||||||
|
-webkit-animation-iteration-count: 1, 1;
|
||||||
|
animation-iteration-count: 1, 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bVgqGk {
|
||||||
|
opacity: 0;
|
||||||
|
stroke-dasharray: 76;
|
||||||
|
-webkit-animation: outerDrawIn 0.5s ease-out forwards 1.1333333333333333s, appearIn 0.1s ease-out forwards 1.1333333333333333s;
|
||||||
|
animation: outerDrawIn 0.5s ease-out forwards 1.1333333333333333s, appearIn 0.1s ease-out forwards 1.1333333333333333s;
|
||||||
|
-webkit-animation-iteration-count: 1, 1;
|
||||||
|
animation-iteration-count: 1, 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hEFqBt {
|
||||||
|
opacity: 0;
|
||||||
|
stroke-dasharray: 76;
|
||||||
|
-webkit-animation: outerDrawIn 0.5s ease-out forwards 1.3333333333333333s, appearIn 0.1s ease-out forwards 1.3333333333333333s;
|
||||||
|
animation: outerDrawIn 0.5s ease-out forwards 1.3333333333333333s, appearIn 0.1s ease-out forwards 1.3333333333333333s;
|
||||||
|
-webkit-animation-iteration-count: 1, 1;
|
||||||
|
animation-iteration-count: 1, 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dzEKCM {
|
||||||
|
opacity: 0;
|
||||||
|
stroke-dasharray: 70;
|
||||||
|
-webkit-animation: innerDrawIn 1s ease-in-out forwards 1.3666666666666667s, appearIn 0.1s linear forwards 1.3666666666666667s;
|
||||||
|
animation: innerDrawIn 1s ease-in-out forwards 1.3666666666666667s, appearIn 0.1s linear forwards 1.3666666666666667s;
|
||||||
|
-webkit-animation-iteration-count: infinite, 1;
|
||||||
|
animation-iteration-count: infinite, 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DYnPx {
|
||||||
|
opacity: 0;
|
||||||
|
stroke-dasharray: 70;
|
||||||
|
-webkit-animation: innerDrawIn 1s ease-in-out forwards 1.5333333333333332s, appearIn 0.1s linear forwards 1.5333333333333332s;
|
||||||
|
animation: innerDrawIn 1s ease-in-out forwards 1.5333333333333332s, appearIn 0.1s linear forwards 1.5333333333333332s;
|
||||||
|
-webkit-animation-iteration-count: infinite, 1;
|
||||||
|
animation-iteration-count: infinite, 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hjPEAQ {
|
||||||
|
opacity: 0;
|
||||||
|
stroke-dasharray: 70;
|
||||||
|
-webkit-animation: innerDrawIn 1s ease-in-out forwards 1.7000000000000002s, appearIn 0.1s linear forwards 1.7000000000000002s;
|
||||||
|
animation: innerDrawIn 1s ease-in-out forwards 1.7000000000000002s, appearIn 0.1s linear forwards 1.7000000000000002s;
|
||||||
|
-webkit-animation-iteration-count: infinite, 1;
|
||||||
|
animation-iteration-count: infinite, 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
-webkit-align-items: center;
|
||||||
|
-webkit-box-align: center;
|
||||||
|
-ms-flex-align: center;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-box-pack: center;
|
||||||
|
-webkit-justify-content: center;
|
||||||
|
-ms-flex-pack: center;
|
||||||
|
justify-content: center;
|
||||||
|
-webkit-flex-direction: column;
|
||||||
|
-ms-flex-direction: column;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 75px;
|
||||||
|
height: 75px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-animation: fadeIn 0.5s ease-out forwards;
|
||||||
|
animation: fadeIn 0.5s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 200;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-animation: fadeIn 0.5s ease-out forwards;
|
||||||
|
animation: fadeIn 0.5s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dGfHfc {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="loading-wrapper">
|
||||||
|
<svg class="logo" viewBox="0 0 128 128" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<title>GraphQL Playground Logo</title>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="linearGradient-1" x1="4.86%" x2="96.21%" y1="0%" y2="99.66%">
|
||||||
|
<stop stop-color="#E00082" stop-opacity=".8" offset="0%"></stop>
|
||||||
|
<stop stop-color="#E00082" offset="100%"></stop>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<rect id="Gradient" width="127.96" height="127.96" y="1" fill="url(#linearGradient-1)" rx="4"></rect>
|
||||||
|
<path id="Border" fill="#E00082" fill-rule="nonzero" d="M4.7 2.84c-1.58 0-2.86 1.28-2.86 2.85v116.57c0 1.57 1.28 2.84 2.85 2.84h116.57c1.57 0 2.84-1.26 2.84-2.83V5.67c0-1.55-1.26-2.83-2.83-2.83H4.67zM4.7 0h116.58c3.14 0 5.68 2.55 5.68 5.7v116.58c0 3.14-2.54 5.68-5.68 5.68H4.68c-3.13 0-5.68-2.54-5.68-5.68V5.68C-1 2.56 1.55 0 4.7 0z"></path>
|
||||||
|
<path class="bglIGM" x="64" y="28" fill="#fff" d="M64 36c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8" style="transform: translate(100px, 100px);"></path>
|
||||||
|
<path class="ksxRII" x="95.98500061035156" y="46.510000228881836" fill="#fff" d="M89.04 50.52c-2.2-3.84-.9-8.73 2.94-10.96 3.83-2.2 8.72-.9 10.95 2.94 2.2 3.84.9 8.73-2.94 10.96-3.85 2.2-8.76.9-10.97-2.94"
|
||||||
|
style="transform: translate(100px, 100px);"></path>
|
||||||
|
<path class="cWrBmb" x="95.97162628173828" y="83.4900016784668" fill="#fff" d="M102.9 87.5c-2.2 3.84-7.1 5.15-10.94 2.94-3.84-2.2-5.14-7.12-2.94-10.96 2.2-3.84 7.12-5.15 10.95-2.94 3.86 2.23 5.16 7.12 2.94 10.96"
|
||||||
|
style="transform: translate(100px, 100px);"></path>
|
||||||
|
<path class="Wnusb" x="64" y="101.97999572753906" fill="#fff" d="M64 110c-4.43 0-8-3.6-8-8.02 0-4.44 3.57-8.02 8-8.02s8 3.58 8 8.02c0 4.4-3.57 8.02-8 8.02"
|
||||||
|
style="transform: translate(100px, 100px);"></path>
|
||||||
|
<path class="bfPqf" x="32.03982162475586" y="83.4900016784668" fill="#fff" d="M25.1 87.5c-2.2-3.84-.9-8.73 2.93-10.96 3.83-2.2 8.72-.9 10.95 2.94 2.2 3.84.9 8.73-2.94 10.96-3.85 2.2-8.74.9-10.95-2.94"
|
||||||
|
style="transform: translate(100px, 100px);"></path>
|
||||||
|
<path class="edRCTN" x="32.033552169799805" y="46.510000228881836" fill="#fff" d="M38.96 50.52c-2.2 3.84-7.12 5.15-10.95 2.94-3.82-2.2-5.12-7.12-2.92-10.96 2.2-3.84 7.12-5.15 10.95-2.94 3.83 2.23 5.14 7.12 2.94 10.96"
|
||||||
|
style="transform: translate(100px, 100px);"></path>
|
||||||
|
<path class="iEGVWn" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M63.55 27.5l32.9 19-32.9-19z"></path>
|
||||||
|
<path class="bsocdx" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M96 46v38-38z"></path>
|
||||||
|
<path class="jAZXmP" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M96.45 84.5l-32.9 19 32.9-19z"></path>
|
||||||
|
<path class="hSeArx" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M64.45 103.5l-32.9-19 32.9 19z"></path>
|
||||||
|
<path class="bVgqGk" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M32 84V46v38z"></path>
|
||||||
|
<path class="hEFqBt" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M31.55 46.5l32.9-19-32.9 19z"></path>
|
||||||
|
<path class="dzEKCM" id="Triangle-Bottom" stroke="#fff" stroke-width="4" d="M30 84h70" stroke-linecap="round"></path>
|
||||||
|
<path class="DYnPx" id="Triangle-Left" stroke="#fff" stroke-width="4" d="M65 26L30 87" stroke-linecap="round"></path>
|
||||||
|
<path class="hjPEAQ" id="Triangle-Right" stroke="#fff" stroke-width="4" d="M98 87L63 26" stroke-linecap="round"></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<div class="text">Loading
|
||||||
|
<span class="dGfHfc">GraphQL Playground</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="root" />
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.addEventListener('load', function (event) {
|
||||||
|
|
||||||
|
const loadingWrapper = document.getElementById('loading-wrapper');
|
||||||
|
loadingWrapper.classList.add('fadeOut');
|
||||||
|
|
||||||
|
|
||||||
|
const root = document.getElementById('root');
|
||||||
|
root.classList.add('playgroundIn');
|
||||||
|
|
||||||
|
GraphQLPlayground.init(root, {
|
||||||
|
endpoint: 'JUNIPER_URL', subscriptionEndpoint: 'JUNIPER_SUBSCRIPTIONS_URL'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,7 +1,6 @@
|
||||||
//! Utility module to generate a GraphQL Playground interface
|
//! Utility module to generate a GraphQL Playground interface.
|
||||||
|
|
||||||
/// Generate the HTML source to show a GraphQL Playground interface
|
/// Generate the HTML source to show a GraphQL Playground interface.
|
||||||
// source: https://github.com/prisma/graphql-playground/blob/master/packages/graphql-playground-html/withAnimation.html
|
|
||||||
pub fn playground_source(
|
pub fn playground_source(
|
||||||
graphql_endpoint_url: &str,
|
graphql_endpoint_url: &str,
|
||||||
subscriptions_endpoint_url: Option<&str>,
|
subscriptions_endpoint_url: Option<&str>,
|
||||||
|
@ -12,545 +11,7 @@ pub fn playground_source(
|
||||||
graphql_endpoint_url
|
graphql_endpoint_url
|
||||||
};
|
};
|
||||||
|
|
||||||
r##"
|
include_str!("playground.html")
|
||||||
<!DOCTYPE html>
|
.replace("JUNIPER_URL", graphql_endpoint_url)
|
||||||
|
.replace("JUNIPER_SUBSCRIPTIONS_URL", subscriptions_endpoint)
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset=utf-8 />
|
|
||||||
<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
|
|
||||||
<title>GraphQL Playground</title>
|
|
||||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/graphql-playground-react@1.7.26/build/static/css/index.css" />
|
|
||||||
<link rel="shortcut icon" href="//cdn.jsdelivr.net/npm/graphql-playground-react@1.7.26/build/favicon.png" />
|
|
||||||
<script src="//cdn.jsdelivr.net/npm/graphql-playground-react@1.7.26/build/static/js/middleware.js"></script>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<style type="text/css">
|
|
||||||
html {
|
|
||||||
font-family: "Open Sans", sans-serif;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
background: #172a3a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playgroundIn {
|
|
||||||
-webkit-animation: playgroundIn 0.5s ease-out forwards;
|
|
||||||
animation: playgroundIn 0.5s ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes playgroundIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transform: translateY(10px);
|
|
||||||
-ms-transform: translateY(10px);
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: translateY(0);
|
|
||||||
-ms-transform: translateY(0);
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes playgroundIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transform: translateY(10px);
|
|
||||||
-ms-transform: translateY(10px);
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: translateY(0);
|
|
||||||
-ms-transform: translateY(0);
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style type="text/css">
|
|
||||||
.fadeOut {
|
|
||||||
-webkit-animation: fadeOut 0.5s ease-out forwards;
|
|
||||||
animation: fadeOut 0.5s ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transform: translateY(-10px);
|
|
||||||
-ms-transform: translateY(-10px);
|
|
||||||
transform: translateY(-10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: translateY(0);
|
|
||||||
-ms-transform: translateY(0);
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transform: translateY(-10px);
|
|
||||||
-ms-transform: translateY(-10px);
|
|
||||||
transform: translateY(-10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: translateY(0);
|
|
||||||
-ms-transform: translateY(0);
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes fadeOut {
|
|
||||||
from {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: translateY(0);
|
|
||||||
-ms-transform: translateY(0);
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transform: translateY(-10px);
|
|
||||||
-ms-transform: translateY(-10px);
|
|
||||||
transform: translateY(-10px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeOut {
|
|
||||||
from {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: translateY(0);
|
|
||||||
-ms-transform: translateY(0);
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transform: translateY(-10px);
|
|
||||||
-ms-transform: translateY(-10px);
|
|
||||||
transform: translateY(-10px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes appearIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transform: translateY(0px);
|
|
||||||
-ms-transform: translateY(0px);
|
|
||||||
transform: translateY(0px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: translateY(0);
|
|
||||||
-ms-transform: translateY(0);
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes appearIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transform: translateY(0px);
|
|
||||||
-ms-transform: translateY(0px);
|
|
||||||
transform: translateY(0px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: translateY(0);
|
|
||||||
-ms-transform: translateY(0);
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes scaleIn {
|
|
||||||
from {
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
-ms-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
-webkit-transform: scale(1);
|
|
||||||
-ms-transform: scale(1);
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scaleIn {
|
|
||||||
from {
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
-ms-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
-webkit-transform: scale(1);
|
|
||||||
-ms-transform: scale(1);
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes innerDrawIn {
|
|
||||||
0% {
|
|
||||||
stroke-dashoffset: 70;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
stroke-dashoffset: 140;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
stroke-dashoffset: 210;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes innerDrawIn {
|
|
||||||
0% {
|
|
||||||
stroke-dashoffset: 70;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
stroke-dashoffset: 140;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
stroke-dashoffset: 210;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes outerDrawIn {
|
|
||||||
0% {
|
|
||||||
stroke-dashoffset: 76;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
stroke-dashoffset: 152;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes outerDrawIn {
|
|
||||||
0% {
|
|
||||||
stroke-dashoffset: 76;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
stroke-dashoffset: 152;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hHWjkv {
|
|
||||||
-webkit-transform-origin: 0px 0px;
|
|
||||||
-ms-transform-origin: 0px 0px;
|
|
||||||
transform-origin: 0px 0px;
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
-ms-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
-webkit-animation: scaleIn 0.25s linear forwards 0.2222222222222222s;
|
|
||||||
animation: scaleIn 0.25s linear forwards 0.2222222222222222s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gCDOzd {
|
|
||||||
-webkit-transform-origin: 0px 0px;
|
|
||||||
-ms-transform-origin: 0px 0px;
|
|
||||||
transform-origin: 0px 0px;
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
-ms-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
-webkit-animation: scaleIn 0.25s linear forwards 0.4222222222222222s;
|
|
||||||
animation: scaleIn 0.25s linear forwards 0.4222222222222222s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hmCcxi {
|
|
||||||
-webkit-transform-origin: 0px 0px;
|
|
||||||
-ms-transform-origin: 0px 0px;
|
|
||||||
transform-origin: 0px 0px;
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
-ms-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
-webkit-animation: scaleIn 0.25s linear forwards 0.6222222222222222s;
|
|
||||||
animation: scaleIn 0.25s linear forwards 0.6222222222222222s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eHamQi {
|
|
||||||
-webkit-transform-origin: 0px 0px;
|
|
||||||
-ms-transform-origin: 0px 0px;
|
|
||||||
transform-origin: 0px 0px;
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
-ms-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
-webkit-animation: scaleIn 0.25s linear forwards 0.8222222222222223s;
|
|
||||||
animation: scaleIn 0.25s linear forwards 0.8222222222222223s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.byhgGu {
|
|
||||||
-webkit-transform-origin: 0px 0px;
|
|
||||||
-ms-transform-origin: 0px 0px;
|
|
||||||
transform-origin: 0px 0px;
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
-ms-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
-webkit-animation: scaleIn 0.25s linear forwards 1.0222222222222221s;
|
|
||||||
animation: scaleIn 0.25s linear forwards 1.0222222222222221s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.llAKP {
|
|
||||||
-webkit-transform-origin: 0px 0px;
|
|
||||||
-ms-transform-origin: 0px 0px;
|
|
||||||
transform-origin: 0px 0px;
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
-ms-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
-webkit-animation: scaleIn 0.25s linear forwards 1.2222222222222223s;
|
|
||||||
animation: scaleIn 0.25s linear forwards 1.2222222222222223s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bglIGM {
|
|
||||||
-webkit-transform-origin: 64px 28px;
|
|
||||||
-ms-transform-origin: 64px 28px;
|
|
||||||
transform-origin: 64px 28px;
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
-ms-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
-webkit-animation: scaleIn 0.25s linear forwards 0.2222222222222222s;
|
|
||||||
animation: scaleIn 0.25s linear forwards 0.2222222222222222s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ksxRII {
|
|
||||||
-webkit-transform-origin: 95.98500061035156px 46.510000228881836px;
|
|
||||||
-ms-transform-origin: 95.98500061035156px 46.510000228881836px;
|
|
||||||
transform-origin: 95.98500061035156px 46.510000228881836px;
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
-ms-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
-webkit-animation: scaleIn 0.25s linear forwards 0.4222222222222222s;
|
|
||||||
animation: scaleIn 0.25s linear forwards 0.4222222222222222s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cWrBmb {
|
|
||||||
-webkit-transform-origin: 95.97162628173828px 83.4900016784668px;
|
|
||||||
-ms-transform-origin: 95.97162628173828px 83.4900016784668px;
|
|
||||||
transform-origin: 95.97162628173828px 83.4900016784668px;
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
-ms-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
-webkit-animation: scaleIn 0.25s linear forwards 0.6222222222222222s;
|
|
||||||
animation: scaleIn 0.25s linear forwards 0.6222222222222222s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Wnusb {
|
|
||||||
-webkit-transform-origin: 64px 101.97999572753906px;
|
|
||||||
-ms-transform-origin: 64px 101.97999572753906px;
|
|
||||||
transform-origin: 64px 101.97999572753906px;
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
-ms-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
-webkit-animation: scaleIn 0.25s linear forwards 0.8222222222222223s;
|
|
||||||
animation: scaleIn 0.25s linear forwards 0.8222222222222223s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bfPqf {
|
|
||||||
-webkit-transform-origin: 32.03982162475586px 83.4900016784668px;
|
|
||||||
-ms-transform-origin: 32.03982162475586px 83.4900016784668px;
|
|
||||||
transform-origin: 32.03982162475586px 83.4900016784668px;
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
-ms-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
-webkit-animation: scaleIn 0.25s linear forwards 1.0222222222222221s;
|
|
||||||
animation: scaleIn 0.25s linear forwards 1.0222222222222221s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edRCTN {
|
|
||||||
-webkit-transform-origin: 32.033552169799805px 46.510000228881836px;
|
|
||||||
-ms-transform-origin: 32.033552169799805px 46.510000228881836px;
|
|
||||||
transform-origin: 32.033552169799805px 46.510000228881836px;
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
-ms-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
-webkit-animation: scaleIn 0.25s linear forwards 1.2222222222222223s;
|
|
||||||
animation: scaleIn 0.25s linear forwards 1.2222222222222223s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iEGVWn {
|
|
||||||
opacity: 0;
|
|
||||||
stroke-dasharray: 76;
|
|
||||||
-webkit-animation: outerDrawIn 0.5s ease-out forwards 0.3333333333333333s, appearIn 0.1s ease-out forwards 0.3333333333333333s;
|
|
||||||
animation: outerDrawIn 0.5s ease-out forwards 0.3333333333333333s, appearIn 0.1s ease-out forwards 0.3333333333333333s;
|
|
||||||
-webkit-animation-iteration-count: 1, 1;
|
|
||||||
animation-iteration-count: 1, 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bsocdx {
|
|
||||||
opacity: 0;
|
|
||||||
stroke-dasharray: 76;
|
|
||||||
-webkit-animation: outerDrawIn 0.5s ease-out forwards 0.5333333333333333s, appearIn 0.1s ease-out forwards 0.5333333333333333s;
|
|
||||||
animation: outerDrawIn 0.5s ease-out forwards 0.5333333333333333s, appearIn 0.1s ease-out forwards 0.5333333333333333s;
|
|
||||||
-webkit-animation-iteration-count: 1, 1;
|
|
||||||
animation-iteration-count: 1, 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jAZXmP {
|
|
||||||
opacity: 0;
|
|
||||||
stroke-dasharray: 76;
|
|
||||||
-webkit-animation: outerDrawIn 0.5s ease-out forwards 0.7333333333333334s, appearIn 0.1s ease-out forwards 0.7333333333333334s;
|
|
||||||
animation: outerDrawIn 0.5s ease-out forwards 0.7333333333333334s, appearIn 0.1s ease-out forwards 0.7333333333333334s;
|
|
||||||
-webkit-animation-iteration-count: 1, 1;
|
|
||||||
animation-iteration-count: 1, 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hSeArx {
|
|
||||||
opacity: 0;
|
|
||||||
stroke-dasharray: 76;
|
|
||||||
-webkit-animation: outerDrawIn 0.5s ease-out forwards 0.9333333333333333s, appearIn 0.1s ease-out forwards 0.9333333333333333s;
|
|
||||||
animation: outerDrawIn 0.5s ease-out forwards 0.9333333333333333s, appearIn 0.1s ease-out forwards 0.9333333333333333s;
|
|
||||||
-webkit-animation-iteration-count: 1, 1;
|
|
||||||
animation-iteration-count: 1, 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bVgqGk {
|
|
||||||
opacity: 0;
|
|
||||||
stroke-dasharray: 76;
|
|
||||||
-webkit-animation: outerDrawIn 0.5s ease-out forwards 1.1333333333333333s, appearIn 0.1s ease-out forwards 1.1333333333333333s;
|
|
||||||
animation: outerDrawIn 0.5s ease-out forwards 1.1333333333333333s, appearIn 0.1s ease-out forwards 1.1333333333333333s;
|
|
||||||
-webkit-animation-iteration-count: 1, 1;
|
|
||||||
animation-iteration-count: 1, 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hEFqBt {
|
|
||||||
opacity: 0;
|
|
||||||
stroke-dasharray: 76;
|
|
||||||
-webkit-animation: outerDrawIn 0.5s ease-out forwards 1.3333333333333333s, appearIn 0.1s ease-out forwards 1.3333333333333333s;
|
|
||||||
animation: outerDrawIn 0.5s ease-out forwards 1.3333333333333333s, appearIn 0.1s ease-out forwards 1.3333333333333333s;
|
|
||||||
-webkit-animation-iteration-count: 1, 1;
|
|
||||||
animation-iteration-count: 1, 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dzEKCM {
|
|
||||||
opacity: 0;
|
|
||||||
stroke-dasharray: 70;
|
|
||||||
-webkit-animation: innerDrawIn 1s ease-in-out forwards 1.3666666666666667s, appearIn 0.1s linear forwards 1.3666666666666667s;
|
|
||||||
animation: innerDrawIn 1s ease-in-out forwards 1.3666666666666667s, appearIn 0.1s linear forwards 1.3666666666666667s;
|
|
||||||
-webkit-animation-iteration-count: infinite, 1;
|
|
||||||
animation-iteration-count: infinite, 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.DYnPx {
|
|
||||||
opacity: 0;
|
|
||||||
stroke-dasharray: 70;
|
|
||||||
-webkit-animation: innerDrawIn 1s ease-in-out forwards 1.5333333333333332s, appearIn 0.1s linear forwards 1.5333333333333332s;
|
|
||||||
animation: innerDrawIn 1s ease-in-out forwards 1.5333333333333332s, appearIn 0.1s linear forwards 1.5333333333333332s;
|
|
||||||
-webkit-animation-iteration-count: infinite, 1;
|
|
||||||
animation-iteration-count: infinite, 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hjPEAQ {
|
|
||||||
opacity: 0;
|
|
||||||
stroke-dasharray: 70;
|
|
||||||
-webkit-animation: innerDrawIn 1s ease-in-out forwards 1.7000000000000002s, appearIn 0.1s linear forwards 1.7000000000000002s;
|
|
||||||
animation: innerDrawIn 1s ease-in-out forwards 1.7000000000000002s, appearIn 0.1s linear forwards 1.7000000000000002s;
|
|
||||||
-webkit-animation-iteration-count: infinite, 1;
|
|
||||||
animation-iteration-count: infinite, 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loading-wrapper {
|
|
||||||
position: absolute;
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
display: -webkit-box;
|
|
||||||
display: -webkit-flex;
|
|
||||||
display: -ms-flexbox;
|
|
||||||
display: flex;
|
|
||||||
-webkit-align-items: center;
|
|
||||||
-webkit-box-align: center;
|
|
||||||
-ms-flex-align: center;
|
|
||||||
align-items: center;
|
|
||||||
-webkit-box-pack: center;
|
|
||||||
-webkit-justify-content: center;
|
|
||||||
-ms-flex-pack: center;
|
|
||||||
justify-content: center;
|
|
||||||
-webkit-flex-direction: column;
|
|
||||||
-ms-flex-direction: column;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
width: 75px;
|
|
||||||
height: 75px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-animation: fadeIn 0.5s ease-out forwards;
|
|
||||||
animation: fadeIn 0.5s ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 200;
|
|
||||||
text-align: center;
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-animation: fadeIn 0.5s ease-out forwards;
|
|
||||||
animation: fadeIn 0.5s ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dGfHfc {
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div id="loading-wrapper">
|
|
||||||
<svg class="logo" viewBox="0 0 128 128" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<title>GraphQL Playground Logo</title>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="linearGradient-1" x1="4.86%" x2="96.21%" y1="0%" y2="99.66%">
|
|
||||||
<stop stop-color="#E00082" stop-opacity=".8" offset="0%"></stop>
|
|
||||||
<stop stop-color="#E00082" offset="100%"></stop>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<g>
|
|
||||||
<rect id="Gradient" width="127.96" height="127.96" y="1" fill="url(#linearGradient-1)" rx="4"></rect>
|
|
||||||
<path id="Border" fill="#E00082" fill-rule="nonzero" d="M4.7 2.84c-1.58 0-2.86 1.28-2.86 2.85v116.57c0 1.57 1.28 2.84 2.85 2.84h116.57c1.57 0 2.84-1.26 2.84-2.83V5.67c0-1.55-1.26-2.83-2.83-2.83H4.67zM4.7 0h116.58c3.14 0 5.68 2.55 5.68 5.7v116.58c0 3.14-2.54 5.68-5.68 5.68H4.68c-3.13 0-5.68-2.54-5.68-5.68V5.68C-1 2.56 1.55 0 4.7 0z"></path>
|
|
||||||
<path class="bglIGM" x="64" y="28" fill="#fff" d="M64 36c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8" style="transform: translate(100px, 100px);"></path>
|
|
||||||
<path class="ksxRII" x="95.98500061035156" y="46.510000228881836" fill="#fff" d="M89.04 50.52c-2.2-3.84-.9-8.73 2.94-10.96 3.83-2.2 8.72-.9 10.95 2.94 2.2 3.84.9 8.73-2.94 10.96-3.85 2.2-8.76.9-10.97-2.94"
|
|
||||||
style="transform: translate(100px, 100px);"></path>
|
|
||||||
<path class="cWrBmb" x="95.97162628173828" y="83.4900016784668" fill="#fff" d="M102.9 87.5c-2.2 3.84-7.1 5.15-10.94 2.94-3.84-2.2-5.14-7.12-2.94-10.96 2.2-3.84 7.12-5.15 10.95-2.94 3.86 2.23 5.16 7.12 2.94 10.96"
|
|
||||||
style="transform: translate(100px, 100px);"></path>
|
|
||||||
<path class="Wnusb" x="64" y="101.97999572753906" fill="#fff" d="M64 110c-4.43 0-8-3.6-8-8.02 0-4.44 3.57-8.02 8-8.02s8 3.58 8 8.02c0 4.4-3.57 8.02-8 8.02"
|
|
||||||
style="transform: translate(100px, 100px);"></path>
|
|
||||||
<path class="bfPqf" x="32.03982162475586" y="83.4900016784668" fill="#fff" d="M25.1 87.5c-2.2-3.84-.9-8.73 2.93-10.96 3.83-2.2 8.72-.9 10.95 2.94 2.2 3.84.9 8.73-2.94 10.96-3.85 2.2-8.74.9-10.95-2.94"
|
|
||||||
style="transform: translate(100px, 100px);"></path>
|
|
||||||
<path class="edRCTN" x="32.033552169799805" y="46.510000228881836" fill="#fff" d="M38.96 50.52c-2.2 3.84-7.12 5.15-10.95 2.94-3.82-2.2-5.12-7.12-2.92-10.96 2.2-3.84 7.12-5.15 10.95-2.94 3.83 2.23 5.14 7.12 2.94 10.96"
|
|
||||||
style="transform: translate(100px, 100px);"></path>
|
|
||||||
<path class="iEGVWn" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M63.55 27.5l32.9 19-32.9-19z"></path>
|
|
||||||
<path class="bsocdx" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M96 46v38-38z"></path>
|
|
||||||
<path class="jAZXmP" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M96.45 84.5l-32.9 19 32.9-19z"></path>
|
|
||||||
<path class="hSeArx" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M64.45 103.5l-32.9-19 32.9 19z"></path>
|
|
||||||
<path class="bVgqGk" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M32 84V46v38z"></path>
|
|
||||||
<path class="hEFqBt" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M31.55 46.5l32.9-19-32.9 19z"></path>
|
|
||||||
<path class="dzEKCM" id="Triangle-Bottom" stroke="#fff" stroke-width="4" d="M30 84h70" stroke-linecap="round"></path>
|
|
||||||
<path class="DYnPx" id="Triangle-Left" stroke="#fff" stroke-width="4" d="M65 26L30 87" stroke-linecap="round"></path>
|
|
||||||
<path class="hjPEAQ" id="Triangle-Right" stroke="#fff" stroke-width="4" d="M98 87L63 26" stroke-linecap="round"></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<div class="text">Loading
|
|
||||||
<span class="dGfHfc">GraphQL Playground</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="root" />
|
|
||||||
<script type="text/javascript">
|
|
||||||
window.addEventListener('load', function (event) {
|
|
||||||
|
|
||||||
const loadingWrapper = document.getElementById('loading-wrapper');
|
|
||||||
loadingWrapper.classList.add('fadeOut');
|
|
||||||
|
|
||||||
|
|
||||||
const root = document.getElementById('root');
|
|
||||||
root.classList.add('playgroundIn');
|
|
||||||
|
|
||||||
GraphQLPlayground.init(root, { endpoint: 'JUNIPER_GRAPHQL_URL', subscriptionEndpoint: 'JUNIPER_SUBSCRIPTIONS_URL' })
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"##.replace("JUNIPER_GRAPHQL_URL", graphql_endpoint_url)
|
|
||||||
.replace("JUNIPER_SUBSCRIPTIONS_URL", subscriptions_endpoint)
|
|
||||||
}
|
}
|
||||||
|
|
131
juniper/src/integrations/anyhow.rs
Normal file
131
juniper/src/integrations/anyhow.rs
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
//! GraphQL support for [`anyhow::Error`].
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! # use std::backtrace::Backtrace;
|
||||||
|
//! use anyhow::anyhow;
|
||||||
|
//! # use juniper::graphql_object;
|
||||||
|
//!
|
||||||
|
//! struct Root;
|
||||||
|
//!
|
||||||
|
//! #[graphql_object]
|
||||||
|
//! impl Root {
|
||||||
|
//! fn err() -> anyhow::Result<i32> {
|
||||||
|
//! Err(anyhow!("errored!"))
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! # Backtrace
|
||||||
|
//!
|
||||||
|
//! Backtrace is supported in the same way as [`anyhow`] crate does:
|
||||||
|
//! > If using the nightly channel, or stable with `features = ["backtrace"]`, a backtrace is
|
||||||
|
//! > captured and printed with the error if the underlying error type does not already provide its
|
||||||
|
//! > own. In order to see backtraces, they must be enabled through the environment variables
|
||||||
|
//! > described in [`std::backtrace`]:
|
||||||
|
//! > - If you want panics and errors to both have backtraces, set `RUST_BACKTRACE=1`;
|
||||||
|
//! > - If you want only errors to have backtraces, set `RUST_LIB_BACKTRACE=1`;
|
||||||
|
//! > - If you want only panics to have backtraces, set `RUST_BACKTRACE=1` and
|
||||||
|
//! > `RUST_LIB_BACKTRACE=0`.
|
||||||
|
|
||||||
|
use crate::{FieldError, IntoFieldError, ScalarValue, Value};
|
||||||
|
|
||||||
|
impl<S: ScalarValue> IntoFieldError<S> for anyhow::Error {
|
||||||
|
fn into_field_error(self) -> FieldError<S> {
|
||||||
|
#[cfg(any(nightly, feature = "backtrace"))]
|
||||||
|
let extensions = {
|
||||||
|
let backtrace = self.backtrace().to_string();
|
||||||
|
if backtrace == "disabled backtrace" {
|
||||||
|
Value::Null
|
||||||
|
} else {
|
||||||
|
let mut obj = crate::value::Object::with_capacity(1);
|
||||||
|
_ = obj.add_field(
|
||||||
|
"backtrace",
|
||||||
|
Value::List(
|
||||||
|
backtrace
|
||||||
|
.split('\n')
|
||||||
|
.map(|line| Value::Scalar(line.to_owned().into()))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Value::Object(obj)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#[cfg(not(any(nightly, feature = "backtrace")))]
|
||||||
|
let extensions = Value::Null;
|
||||||
|
|
||||||
|
FieldError::new(self, extensions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use serial_test::serial;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
execute, graphql_object, graphql_value, graphql_vars, parser::SourcePosition,
|
||||||
|
EmptyMutation, EmptySubscription, RootNode,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn simple() {
|
||||||
|
struct Root;
|
||||||
|
|
||||||
|
#[graphql_object]
|
||||||
|
impl Root {
|
||||||
|
fn err() -> anyhow::Result<i32> {
|
||||||
|
Err(anyhow!("errored!"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prev_env = env::var("RUST_BACKTRACE").ok();
|
||||||
|
env::set_var("RUST_BACKTRACE", "1");
|
||||||
|
|
||||||
|
const DOC: &str = r#"{
|
||||||
|
err
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let schema = RootNode::new(
|
||||||
|
Root,
|
||||||
|
EmptyMutation::<()>::new(),
|
||||||
|
EmptySubscription::<()>::new(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let res = execute(DOC, None, &schema, &graphql_vars! {}, &()).await;
|
||||||
|
|
||||||
|
assert!(res.is_ok(), "failed: {:?}", res.unwrap_err());
|
||||||
|
|
||||||
|
let (val, errs) = res.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(val, graphql_value!(null));
|
||||||
|
assert_eq!(errs.len(), 1, "too many errors: {errs:?}");
|
||||||
|
|
||||||
|
let err = errs.first().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(*err.location(), SourcePosition::new(14, 1, 12));
|
||||||
|
assert_eq!(err.path(), &["err"]);
|
||||||
|
|
||||||
|
let err = err.error();
|
||||||
|
|
||||||
|
assert_eq!(err.message(), "errored!");
|
||||||
|
#[cfg(not(any(nightly, feature = "backtrace")))]
|
||||||
|
assert_eq!(err.extensions(), &graphql_value!(null));
|
||||||
|
#[cfg(any(nightly, feature = "backtrace"))]
|
||||||
|
assert_eq!(
|
||||||
|
err.extensions()
|
||||||
|
.as_object_value()
|
||||||
|
.map(|ext| ext.contains_field("backtrace")),
|
||||||
|
Some(true),
|
||||||
|
"no `backtrace` in extensions: {err:?}",
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(val) = prev_env {
|
||||||
|
env::set_var("RUST_BACKTRACE", val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,7 +42,10 @@ mod bigdecimal_scalar {
|
||||||
if let Some(i) = v.as_int_value() {
|
if let Some(i) = v.as_int_value() {
|
||||||
Ok(BigDecimal::from(i))
|
Ok(BigDecimal::from(i))
|
||||||
} else if let Some(f) = v.as_float_value() {
|
} else if let Some(f) = v.as_float_value() {
|
||||||
BigDecimal::try_from(f)
|
// See akubera/bigdecimal-rs#103 for details:
|
||||||
|
// https://github.com/akubera/bigdecimal-rs/issues/103
|
||||||
|
let mut buf = ryu::Buffer::new();
|
||||||
|
BigDecimal::from_str(buf.format(f))
|
||||||
.map_err(|e| format!("Failed to parse `BigDecimal` from `Float`: {e}"))
|
.map_err(|e| format!("Failed to parse `BigDecimal` from `Float`: {e}"))
|
||||||
} else {
|
} else {
|
||||||
v.as_string_value()
|
v.as_string_value()
|
||||||
|
|
|
@ -1,8 +1,35 @@
|
||||||
//! GraphQL support for [bson](https://github.com/mongodb/bson-rust) types.
|
//! GraphQL support for [`bson`] crate types.
|
||||||
|
//!
|
||||||
|
//! # Supported types
|
||||||
|
//!
|
||||||
|
//! | Rust type | Format | GraphQL scalar |
|
||||||
|
//! |-------------------|-------------------|------------------|
|
||||||
|
//! | [`oid::ObjectId`] | HEX string | [`ObjectID`][s1] |
|
||||||
|
//! | [`DateTime`] | [RFC 3339] string | [`DateTime`][s4] |
|
||||||
|
//!
|
||||||
|
//! [`DateTime`]: bson::DateTime
|
||||||
|
//! [`oid::ObjectId`]: bson::oid::ObjectId
|
||||||
|
//! [RFC 3339]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
|
||||||
|
//! [s1]: https://graphql-scalars.dev/docs/scalars/object-id
|
||||||
|
//! [s4]: https://graphql-scalars.dev/docs/scalars/date-time
|
||||||
|
|
||||||
use crate::{graphql_scalar, InputValue, ScalarValue, Value};
|
use crate::{graphql_scalar, InputValue, ScalarValue, Value};
|
||||||
|
|
||||||
#[graphql_scalar(with = object_id, parse_token(String))]
|
/// [BSON ObjectId][0] represented as a HEX string.
|
||||||
|
///
|
||||||
|
/// [`ObjectID` scalar][1] compliant.
|
||||||
|
///
|
||||||
|
/// See also [`bson::oid::ObjectId`][2] for details.
|
||||||
|
///
|
||||||
|
/// [0]: https://www.mongodb.com/docs/manual/reference/bson-types#objectid
|
||||||
|
/// [1]: https://graphql-scalars.dev/docs/scalars/object-id
|
||||||
|
/// [2]: https://docs.rs/bson/*/bson/oid/struct.ObjectId.html
|
||||||
|
#[graphql_scalar(
|
||||||
|
name = "ObjectID",
|
||||||
|
with = object_id,
|
||||||
|
parse_token(String),
|
||||||
|
specified_by_url = "https://graphql-scalars.dev/docs/scalars/object-id",
|
||||||
|
)]
|
||||||
type ObjectId = bson::oid::ObjectId;
|
type ObjectId = bson::oid::ObjectId;
|
||||||
|
|
||||||
mod object_id {
|
mod object_id {
|
||||||
|
@ -16,37 +43,54 @@ mod object_id {
|
||||||
v.as_string_value()
|
v.as_string_value()
|
||||||
.ok_or_else(|| format!("Expected `String`, found: {v}"))
|
.ok_or_else(|| format!("Expected `String`, found: {v}"))
|
||||||
.and_then(|s| {
|
.and_then(|s| {
|
||||||
ObjectId::parse_str(s).map_err(|e| format!("Failed to parse `ObjectId`: {e}"))
|
ObjectId::parse_str(s).map_err(|e| format!("Failed to parse `ObjectID`: {e}"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[graphql_scalar(with = utc_date_time, parse_token(String))]
|
/// [BSON date][3] in [RFC 3339][0] format.
|
||||||
type UtcDateTime = bson::DateTime;
|
///
|
||||||
|
/// [BSON datetimes][3] have millisecond precision and are always in UTC (inputs with other
|
||||||
|
/// timezones are coerced).
|
||||||
|
///
|
||||||
|
/// [`DateTime` scalar][1] compliant.
|
||||||
|
///
|
||||||
|
/// See also [`bson::DateTime`][2] for details.
|
||||||
|
///
|
||||||
|
/// [0]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
|
||||||
|
/// [1]: https://graphql-scalars.dev/docs/scalars/date-time
|
||||||
|
/// [2]: https://docs.rs/bson/*/bson/struct.DateTime.html
|
||||||
|
/// [3]: https://www.mongodb.com/docs/manual/reference/bson-types#date
|
||||||
|
#[graphql_scalar(
|
||||||
|
with = date_time,
|
||||||
|
parse_token(String),
|
||||||
|
specified_by_url = "https://graphql-scalars.dev/docs/scalars/date-time",
|
||||||
|
)]
|
||||||
|
type DateTime = bson::DateTime;
|
||||||
|
|
||||||
mod utc_date_time {
|
mod date_time {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub(super) fn to_output<S: ScalarValue>(v: &UtcDateTime) -> Value<S> {
|
pub(super) fn to_output<S: ScalarValue>(v: &DateTime) -> Value<S> {
|
||||||
Value::scalar(
|
Value::scalar(
|
||||||
(*v).try_to_rfc3339_string()
|
(*v).try_to_rfc3339_string()
|
||||||
.unwrap_or_else(|e| panic!("failed to format `UtcDateTime` as RFC3339: {e}")),
|
.unwrap_or_else(|e| panic!("failed to format `DateTime` as RFC 3339: {e}")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<UtcDateTime, String> {
|
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<DateTime, String> {
|
||||||
v.as_string_value()
|
v.as_string_value()
|
||||||
.ok_or_else(|| format!("Expected `String`, found: {v}"))
|
.ok_or_else(|| format!("Expected `String`, found: {v}"))
|
||||||
.and_then(|s| {
|
.and_then(|s| {
|
||||||
UtcDateTime::parse_rfc3339_str(s)
|
DateTime::parse_rfc3339_str(s)
|
||||||
.map_err(|e| format!("Failed to parse `UtcDateTime`: {e}"))
|
.map_err(|e| format!("Failed to parse `DateTime`: {e}"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use bson::{oid::ObjectId, DateTime as UtcDateTime};
|
use bson::oid::ObjectId;
|
||||||
|
|
||||||
use crate::{graphql_input_value, FromInputValue, InputValue};
|
use crate::{graphql_input_value, FromInputValue, InputValue};
|
||||||
|
|
||||||
|
@ -60,21 +104,161 @@ mod test {
|
||||||
|
|
||||||
assert_eq!(parsed, id);
|
assert_eq!(parsed, id);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod date_time_test {
|
||||||
|
use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
|
||||||
|
|
||||||
|
use super::DateTime;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn utcdatetime_from_input() {
|
fn parses_correct_input() {
|
||||||
use chrono::{DateTime, Utc};
|
for (raw, expected) in [
|
||||||
|
(
|
||||||
|
"2014-11-28T21:00:09+09:00",
|
||||||
|
DateTime::builder()
|
||||||
|
.year(2014)
|
||||||
|
.month(11)
|
||||||
|
.day(28)
|
||||||
|
.hour(12)
|
||||||
|
.second(9)
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2014-11-28T21:00:09Z",
|
||||||
|
DateTime::builder()
|
||||||
|
.year(2014)
|
||||||
|
.month(11)
|
||||||
|
.day(28)
|
||||||
|
.hour(21)
|
||||||
|
.second(9)
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2014-11-28 21:00:09z",
|
||||||
|
DateTime::builder()
|
||||||
|
.year(2014)
|
||||||
|
.month(11)
|
||||||
|
.day(28)
|
||||||
|
.hour(21)
|
||||||
|
.second(9)
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2014-11-28T21:00:09+00:00",
|
||||||
|
DateTime::builder()
|
||||||
|
.year(2014)
|
||||||
|
.month(11)
|
||||||
|
.day(28)
|
||||||
|
.hour(21)
|
||||||
|
.second(9)
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2014-11-28T21:00:09.05+09:00",
|
||||||
|
DateTime::builder()
|
||||||
|
.year(2014)
|
||||||
|
.month(11)
|
||||||
|
.day(28)
|
||||||
|
.hour(12)
|
||||||
|
.second(9)
|
||||||
|
.millisecond(50)
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2014-11-28 21:00:09.05+09:00",
|
||||||
|
DateTime::builder()
|
||||||
|
.year(2014)
|
||||||
|
.month(11)
|
||||||
|
.day(28)
|
||||||
|
.hour(12)
|
||||||
|
.second(9)
|
||||||
|
.millisecond(50)
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let input: InputValue = graphql_input_value!((raw));
|
||||||
|
let parsed = DateTime::from_input_value(&input);
|
||||||
|
|
||||||
let raw = "2020-03-23T17:38:32.446+00:00";
|
assert!(
|
||||||
let input: InputValue = graphql_input_value!((raw));
|
parsed.is_ok(),
|
||||||
|
"failed to parse `{raw}`: {:?}",
|
||||||
|
parsed.unwrap_err(),
|
||||||
|
);
|
||||||
|
assert_eq!(parsed.unwrap(), expected, "input: {raw}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let parsed: UtcDateTime = FromInputValue::from_input_value(&input).unwrap();
|
#[test]
|
||||||
let date_time = UtcDateTime::from_chrono(
|
fn fails_on_invalid_input() {
|
||||||
DateTime::parse_from_rfc3339(raw)
|
for input in [
|
||||||
.unwrap()
|
graphql_input_value!("12"),
|
||||||
.with_timezone(&Utc),
|
graphql_input_value!("12:"),
|
||||||
);
|
graphql_input_value!("56:34:22"),
|
||||||
|
graphql_input_value!("56:34:22.000"),
|
||||||
|
graphql_input_value!("1996-12-1914:23:43"),
|
||||||
|
graphql_input_value!("1996-12-19T14:23:43"),
|
||||||
|
graphql_input_value!("1996-12-19T14:23:43ZZ"),
|
||||||
|
graphql_input_value!("1996-12-19T14:23:43.543"),
|
||||||
|
graphql_input_value!("1996-12-19T14:23"),
|
||||||
|
graphql_input_value!("1996-12-19T14:23:1"),
|
||||||
|
graphql_input_value!("1996-12-19T14:23:"),
|
||||||
|
graphql_input_value!("1996-12-19T23:78:43Z"),
|
||||||
|
graphql_input_value!("1996-12-19T23:18:99Z"),
|
||||||
|
graphql_input_value!("1996-12-19T24:00:00Z"),
|
||||||
|
graphql_input_value!("1996-12-19T99:02:13Z"),
|
||||||
|
graphql_input_value!("1996-12-19T99:02:13Z"),
|
||||||
|
graphql_input_value!("1996-12-19T12:02:13+4444444"),
|
||||||
|
graphql_input_value!("i'm not even a datetime"),
|
||||||
|
graphql_input_value!(2.32),
|
||||||
|
graphql_input_value!(1),
|
||||||
|
graphql_input_value!(null),
|
||||||
|
graphql_input_value!(false),
|
||||||
|
] {
|
||||||
|
let input: InputValue = input;
|
||||||
|
let parsed = DateTime::from_input_value(&input);
|
||||||
|
|
||||||
assert_eq!(parsed, date_time);
|
assert!(parsed.is_err(), "allows input: {input:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_correctly() {
|
||||||
|
for (val, expected) in [
|
||||||
|
(
|
||||||
|
DateTime::builder()
|
||||||
|
.year(1996)
|
||||||
|
.month(12)
|
||||||
|
.day(19)
|
||||||
|
.hour(12)
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
graphql_input_value!("1996-12-19T12:00:00Z"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
DateTime::builder()
|
||||||
|
.year(1564)
|
||||||
|
.month(1)
|
||||||
|
.day(30)
|
||||||
|
.hour(5)
|
||||||
|
.minute(3)
|
||||||
|
.second(3)
|
||||||
|
.millisecond(1)
|
||||||
|
.build()
|
||||||
|
.unwrap(),
|
||||||
|
graphql_input_value!("1564-01-30T05:03:03.001Z"),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let actual: InputValue = val.to_input_value();
|
||||||
|
|
||||||
|
assert_eq!(actual, expected, "on value: {val}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,20 +2,21 @@
|
||||||
//!
|
//!
|
||||||
//! # Supported types
|
//! # Supported types
|
||||||
//!
|
//!
|
||||||
//! | Rust type | Format | GraphQL scalar |
|
//! | Rust type | Format | GraphQL scalar |
|
||||||
//! |-------------------|-----------------------|-------------------|
|
//! |-------------------|-----------------------|-----------------------|
|
||||||
//! | [`NaiveDate`] | `yyyy-MM-dd` | [`Date`][s1] |
|
//! | [`NaiveDate`] | `yyyy-MM-dd` | [`LocalDate`][s1] |
|
||||||
//! | [`NaiveTime`] | `HH:mm[:ss[.SSS]]` | [`LocalTime`][s2] |
|
//! | [`NaiveTime`] | `HH:mm[:ss[.SSS]]` | [`LocalTime`][s2] |
|
||||||
//! | [`NaiveDateTime`] | `yyyy-MM-dd HH:mm:ss` | `LocalDateTime` |
|
//! | [`NaiveDateTime`] | `yyyy-MM-ddTHH:mm:ss` | [`LocalDateTime`][s3] |
|
||||||
//! | [`DateTime`] | [RFC 3339] string | [`DateTime`][s4] |
|
//! | [`DateTime`] | [RFC 3339] string | [`DateTime`][s4] |
|
||||||
//!
|
//!
|
||||||
//! [`DateTime`]: chrono::DateTime
|
//! [`DateTime`]: chrono::DateTime
|
||||||
//! [`NaiveDate`]: chrono::naive::NaiveDate
|
//! [`NaiveDate`]: chrono::naive::NaiveDate
|
||||||
//! [`NaiveDateTime`]: chrono::naive::NaiveDateTime
|
//! [`NaiveDateTime`]: chrono::naive::NaiveDateTime
|
||||||
//! [`NaiveTime`]: chrono::naive::NaiveTime
|
//! [`NaiveTime`]: chrono::naive::NaiveTime
|
||||||
//! [RFC 3339]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
|
//! [RFC 3339]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
|
||||||
//! [s1]: https://graphql-scalars.dev/docs/scalars/date
|
//! [s1]: https://graphql-scalars.dev/docs/scalars/local-date
|
||||||
//! [s2]: https://graphql-scalars.dev/docs/scalars/local-time
|
//! [s2]: https://graphql-scalars.dev/docs/scalars/local-time
|
||||||
|
//! [s3]: https://graphql-scalars.dev/docs/scalars/local-date-time
|
||||||
//! [s4]: https://graphql-scalars.dev/docs/scalars/date-time
|
//! [s4]: https://graphql-scalars.dev/docs/scalars/date-time
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
@ -29,42 +30,43 @@ use crate::{graphql_scalar, InputValue, ScalarValue, Value};
|
||||||
/// Represents a description of the date (as used for birthdays, for example).
|
/// Represents a description of the date (as used for birthdays, for example).
|
||||||
/// It cannot represent an instant on the time-line.
|
/// It cannot represent an instant on the time-line.
|
||||||
///
|
///
|
||||||
/// [`Date` scalar][1] compliant.
|
/// [`LocalDate` scalar][1] compliant.
|
||||||
///
|
///
|
||||||
/// See also [`chrono::NaiveDate`][2] for details.
|
/// See also [`chrono::NaiveDate`][2] for details.
|
||||||
///
|
///
|
||||||
/// [1]: https://graphql-scalars.dev/docs/scalars/date
|
/// [1]: https://graphql-scalars.dev/docs/scalars/local-date
|
||||||
/// [2]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDate.html
|
/// [2]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDate.html
|
||||||
#[graphql_scalar(
|
#[graphql_scalar(
|
||||||
with = date,
|
with = local_date,
|
||||||
parse_token(String),
|
parse_token(String),
|
||||||
specified_by_url = "https://graphql-scalars.dev/docs/scalars/date",
|
specified_by_url = "https://graphql-scalars.dev/docs/scalars/local-date",
|
||||||
)]
|
)]
|
||||||
pub type Date = chrono::NaiveDate;
|
pub type LocalDate = chrono::NaiveDate;
|
||||||
|
|
||||||
mod date {
|
mod local_date {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Format of a [`Date` scalar][1].
|
/// Format of a [`LocalDate` scalar][1].
|
||||||
///
|
///
|
||||||
/// [1]: https://graphql-scalars.dev/docs/scalars/date
|
/// [1]: https://graphql-scalars.dev/docs/scalars/local-date
|
||||||
const FORMAT: &str = "%Y-%m-%d";
|
const FORMAT: &str = "%Y-%m-%d";
|
||||||
|
|
||||||
pub(super) fn to_output<S>(v: &Date) -> Value<S>
|
pub(super) fn to_output<S>(v: &LocalDate) -> Value<S>
|
||||||
where
|
where
|
||||||
S: ScalarValue,
|
S: ScalarValue,
|
||||||
{
|
{
|
||||||
Value::scalar(v.format(FORMAT).to_string())
|
Value::scalar(v.format(FORMAT).to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn from_input<S>(v: &InputValue<S>) -> Result<Date, String>
|
pub(super) fn from_input<S>(v: &InputValue<S>) -> Result<LocalDate, String>
|
||||||
where
|
where
|
||||||
S: ScalarValue,
|
S: ScalarValue,
|
||||||
{
|
{
|
||||||
v.as_string_value()
|
v.as_string_value()
|
||||||
.ok_or_else(|| format!("Expected `String`, found: {v}"))
|
.ok_or_else(|| format!("Expected `String`, found: {v}"))
|
||||||
.and_then(|s| {
|
.and_then(|s| {
|
||||||
Date::parse_from_str(s, FORMAT).map_err(|e| format!("Invalid `Date`: {e}"))
|
LocalDate::parse_from_str(s, FORMAT)
|
||||||
|
.map_err(|e| format!("Invalid `LocalDate`: {e}"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,19 +142,28 @@ mod local_time {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Combined date and time (without time zone) in `yyyy-MM-dd HH:mm:ss` format.
|
/// Combined date and time (without time zone) in `yyyy-MM-ddTHH:mm:ss` format.
|
||||||
///
|
///
|
||||||
/// See also [`chrono::NaiveDateTime`][1] for details.
|
/// [`LocalDateTime` scalar][1] compliant.
|
||||||
///
|
///
|
||||||
/// [1]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDateTime.html
|
/// See also [`chrono::NaiveDateTime`][2] for details.
|
||||||
#[graphql_scalar(with = local_date_time, parse_token(String))]
|
///
|
||||||
|
/// [1]: https://graphql-scalars.dev/docs/scalars/local-date-time
|
||||||
|
/// [2]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDateTime.html
|
||||||
|
#[graphql_scalar(
|
||||||
|
with = local_date_time,
|
||||||
|
parse_token(String),
|
||||||
|
specified_by_url = "https://graphql-scalars.dev/docs/scalars/local-date-time",
|
||||||
|
)]
|
||||||
pub type LocalDateTime = chrono::NaiveDateTime;
|
pub type LocalDateTime = chrono::NaiveDateTime;
|
||||||
|
|
||||||
mod local_date_time {
|
mod local_date_time {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Format of a `LocalDateTime` scalar.
|
/// Format of a [`LocalDateTime` scalar][1].
|
||||||
const FORMAT: &str = "%Y-%m-%d %H:%M:%S";
|
///
|
||||||
|
/// [1]: https://graphql-scalars.dev/docs/scalars/local-date-time
|
||||||
|
const FORMAT: &str = "%Y-%m-%dT%H:%M:%S";
|
||||||
|
|
||||||
pub(super) fn to_output<S>(v: &LocalDateTime) -> Value<S>
|
pub(super) fn to_output<S>(v: &LocalDateTime) -> Value<S>
|
||||||
where
|
where
|
||||||
|
@ -189,6 +200,7 @@ mod local_date_time {
|
||||||
#[graphql_scalar(
|
#[graphql_scalar(
|
||||||
with = date_time,
|
with = date_time,
|
||||||
parse_token(String),
|
parse_token(String),
|
||||||
|
specified_by_url = "https://graphql-scalars.dev/docs/scalars/date-time",
|
||||||
where(
|
where(
|
||||||
Tz: TimeZone + FromFixedOffset,
|
Tz: TimeZone + FromFixedOffset,
|
||||||
Tz::Offset: fmt::Display,
|
Tz::Offset: fmt::Display,
|
||||||
|
@ -329,26 +341,26 @@ impl FromFixedOffset for chrono_tz::Tz {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod date_test {
|
mod local_date_test {
|
||||||
use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
|
use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
|
||||||
|
|
||||||
use super::Date;
|
use super::LocalDate;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_correct_input() {
|
fn parses_correct_input() {
|
||||||
for (raw, expected) in [
|
for (raw, expected) in [
|
||||||
("1996-12-19", Date::from_ymd(1996, 12, 19)),
|
("1996-12-19", LocalDate::from_ymd_opt(1996, 12, 19)),
|
||||||
("1564-01-30", Date::from_ymd(1564, 01, 30)),
|
("1564-01-30", LocalDate::from_ymd_opt(1564, 01, 30)),
|
||||||
] {
|
] {
|
||||||
let input: InputValue = graphql_input_value!((raw));
|
let input: InputValue = graphql_input_value!((raw));
|
||||||
let parsed = Date::from_input_value(&input);
|
let parsed = LocalDate::from_input_value(&input);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
parsed.is_ok(),
|
parsed.is_ok(),
|
||||||
"failed to parse `{raw}`: {:?}",
|
"failed to parse `{raw}`: {:?}",
|
||||||
parsed.unwrap_err(),
|
parsed.unwrap_err(),
|
||||||
);
|
);
|
||||||
assert_eq!(parsed.unwrap(), expected, "input: {raw}");
|
assert_eq!(parsed.unwrap(), expected.unwrap(), "input: {raw}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -369,7 +381,7 @@ mod date_test {
|
||||||
graphql_input_value!(false),
|
graphql_input_value!(false),
|
||||||
] {
|
] {
|
||||||
let input: InputValue = input;
|
let input: InputValue = input;
|
||||||
let parsed = Date::from_input_value(&input);
|
let parsed = LocalDate::from_input_value(&input);
|
||||||
|
|
||||||
assert!(parsed.is_err(), "allows input: {input:?}");
|
assert!(parsed.is_err(), "allows input: {input:?}");
|
||||||
}
|
}
|
||||||
|
@ -379,18 +391,19 @@ mod date_test {
|
||||||
fn formats_correctly() {
|
fn formats_correctly() {
|
||||||
for (val, expected) in [
|
for (val, expected) in [
|
||||||
(
|
(
|
||||||
Date::from_ymd(1996, 12, 19),
|
LocalDate::from_ymd_opt(1996, 12, 19),
|
||||||
graphql_input_value!("1996-12-19"),
|
graphql_input_value!("1996-12-19"),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Date::from_ymd(1564, 01, 30),
|
LocalDate::from_ymd_opt(1564, 01, 30),
|
||||||
graphql_input_value!("1564-01-30"),
|
graphql_input_value!("1564-01-30"),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Date::from_ymd(2020, 01, 01),
|
LocalDate::from_ymd_opt(2020, 01, 01),
|
||||||
graphql_input_value!("2020-01-01"),
|
graphql_input_value!("2020-01-01"),
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
|
let val = val.unwrap();
|
||||||
let actual: InputValue = val.to_input_value();
|
let actual: InputValue = val.to_input_value();
|
||||||
|
|
||||||
assert_eq!(actual, expected, "on value: {val}");
|
assert_eq!(actual, expected, "on value: {val}");
|
||||||
|
@ -407,12 +420,15 @@ mod local_time_test {
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_correct_input() {
|
fn parses_correct_input() {
|
||||||
for (raw, expected) in [
|
for (raw, expected) in [
|
||||||
("14:23:43", LocalTime::from_hms(14, 23, 43)),
|
("14:23:43", LocalTime::from_hms_opt(14, 23, 43)),
|
||||||
("14:00:00", LocalTime::from_hms(14, 00, 00)),
|
("14:00:00", LocalTime::from_hms_opt(14, 00, 00)),
|
||||||
("14:00", LocalTime::from_hms(14, 00, 00)),
|
("14:00", LocalTime::from_hms_opt(14, 00, 00)),
|
||||||
("14:32", LocalTime::from_hms(14, 32, 00)),
|
("14:32", LocalTime::from_hms_opt(14, 32, 00)),
|
||||||
("14:00:00.000", LocalTime::from_hms(14, 00, 00)),
|
("14:00:00.000", LocalTime::from_hms_opt(14, 00, 00)),
|
||||||
("14:23:43.345", LocalTime::from_hms_milli(14, 23, 43, 345)),
|
(
|
||||||
|
"14:23:43.345",
|
||||||
|
LocalTime::from_hms_milli_opt(14, 23, 43, 345),
|
||||||
|
),
|
||||||
] {
|
] {
|
||||||
let input: InputValue = graphql_input_value!((raw));
|
let input: InputValue = graphql_input_value!((raw));
|
||||||
let parsed = LocalTime::from_input_value(&input);
|
let parsed = LocalTime::from_input_value(&input);
|
||||||
|
@ -422,7 +438,7 @@ mod local_time_test {
|
||||||
"failed to parse `{raw}`: {:?}",
|
"failed to parse `{raw}`: {:?}",
|
||||||
parsed.unwrap_err(),
|
parsed.unwrap_err(),
|
||||||
);
|
);
|
||||||
assert_eq!(parsed.unwrap(), expected, "input: {raw}");
|
assert_eq!(parsed.unwrap(), expected.unwrap(), "input: {raw}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -457,22 +473,23 @@ mod local_time_test {
|
||||||
fn formats_correctly() {
|
fn formats_correctly() {
|
||||||
for (val, expected) in [
|
for (val, expected) in [
|
||||||
(
|
(
|
||||||
LocalTime::from_hms_micro(1, 2, 3, 4005),
|
LocalTime::from_hms_micro_opt(1, 2, 3, 4005),
|
||||||
graphql_input_value!("01:02:03.004"),
|
graphql_input_value!("01:02:03.004"),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
LocalTime::from_hms(0, 0, 0),
|
LocalTime::from_hms_opt(0, 0, 0),
|
||||||
graphql_input_value!("00:00:00"),
|
graphql_input_value!("00:00:00"),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
LocalTime::from_hms(12, 0, 0),
|
LocalTime::from_hms_opt(12, 0, 0),
|
||||||
graphql_input_value!("12:00:00"),
|
graphql_input_value!("12:00:00"),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
LocalTime::from_hms(1, 2, 3),
|
LocalTime::from_hms_opt(1, 2, 3),
|
||||||
graphql_input_value!("01:02:03"),
|
graphql_input_value!("01:02:03"),
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
|
let val = val.unwrap();
|
||||||
let actual: InputValue = val.to_input_value();
|
let actual: InputValue = val.to_input_value();
|
||||||
|
|
||||||
assert_eq!(actual, expected, "on value: {val}");
|
assert_eq!(actual, expected, "on value: {val}");
|
||||||
|
@ -492,17 +509,17 @@ mod local_date_time_test {
|
||||||
fn parses_correct_input() {
|
fn parses_correct_input() {
|
||||||
for (raw, expected) in [
|
for (raw, expected) in [
|
||||||
(
|
(
|
||||||
"1996-12-19 14:23:43",
|
"1996-12-19T14:23:43",
|
||||||
LocalDateTime::new(
|
LocalDateTime::new(
|
||||||
NaiveDate::from_ymd(1996, 12, 19),
|
NaiveDate::from_ymd_opt(1996, 12, 19).unwrap(),
|
||||||
NaiveTime::from_hms(14, 23, 43),
|
NaiveTime::from_hms_opt(14, 23, 43).unwrap(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"1564-01-30 14:00:00",
|
"1564-01-30T14:00:00",
|
||||||
LocalDateTime::new(
|
LocalDateTime::new(
|
||||||
NaiveDate::from_ymd(1564, 1, 30),
|
NaiveDate::from_ymd_opt(1564, 1, 30).unwrap(),
|
||||||
NaiveTime::from_hms(14, 00, 00),
|
NaiveTime::from_hms_opt(14, 00, 00).unwrap(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
|
@ -525,15 +542,17 @@ mod local_date_time_test {
|
||||||
graphql_input_value!("12:"),
|
graphql_input_value!("12:"),
|
||||||
graphql_input_value!("56:34:22"),
|
graphql_input_value!("56:34:22"),
|
||||||
graphql_input_value!("56:34:22.000"),
|
graphql_input_value!("56:34:22.000"),
|
||||||
graphql_input_value!("1996-12-19T14:23:43"),
|
graphql_input_value!("1996-12-1914:23:43"),
|
||||||
graphql_input_value!("1996-12-19 14:23:43Z"),
|
graphql_input_value!("1996-12-19 14:23:43"),
|
||||||
graphql_input_value!("1996-12-19 14:23:43.543"),
|
graphql_input_value!("1996-12-19Q14:23:43"),
|
||||||
graphql_input_value!("1996-12-19 14:23"),
|
graphql_input_value!("1996-12-19T14:23:43Z"),
|
||||||
graphql_input_value!("1996-12-19 14:23:"),
|
graphql_input_value!("1996-12-19T14:23:43.543"),
|
||||||
graphql_input_value!("1996-12-19 23:78:43"),
|
graphql_input_value!("1996-12-19T14:23"),
|
||||||
graphql_input_value!("1996-12-19 23:18:99"),
|
graphql_input_value!("1996-12-19T14:23:"),
|
||||||
graphql_input_value!("1996-12-19 24:00:00"),
|
graphql_input_value!("1996-12-19T23:78:43"),
|
||||||
graphql_input_value!("1996-12-19 99:02:13"),
|
graphql_input_value!("1996-12-19T23:18:99"),
|
||||||
|
graphql_input_value!("1996-12-19T24:00:00"),
|
||||||
|
graphql_input_value!("1996-12-19T99:02:13"),
|
||||||
graphql_input_value!("i'm not even a datetime"),
|
graphql_input_value!("i'm not even a datetime"),
|
||||||
graphql_input_value!(2.32),
|
graphql_input_value!(2.32),
|
||||||
graphql_input_value!(1),
|
graphql_input_value!(1),
|
||||||
|
@ -552,17 +571,17 @@ mod local_date_time_test {
|
||||||
for (val, expected) in [
|
for (val, expected) in [
|
||||||
(
|
(
|
||||||
LocalDateTime::new(
|
LocalDateTime::new(
|
||||||
NaiveDate::from_ymd(1996, 12, 19),
|
NaiveDate::from_ymd_opt(1996, 12, 19).unwrap(),
|
||||||
NaiveTime::from_hms(0, 0, 0),
|
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
|
||||||
),
|
),
|
||||||
graphql_input_value!("1996-12-19 00:00:00"),
|
graphql_input_value!("1996-12-19T00:00:00"),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
LocalDateTime::new(
|
LocalDateTime::new(
|
||||||
NaiveDate::from_ymd(1564, 1, 30),
|
NaiveDate::from_ymd_opt(1564, 1, 30).unwrap(),
|
||||||
NaiveTime::from_hms(14, 0, 0),
|
NaiveTime::from_hms_opt(14, 0, 0).unwrap(),
|
||||||
),
|
),
|
||||||
graphql_input_value!("1564-01-30 14:00:00"),
|
graphql_input_value!("1564-01-30T14:00:00"),
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
let actual: InputValue = val.to_input_value();
|
let actual: InputValue = val.to_input_value();
|
||||||
|
@ -588,42 +607,62 @@ mod date_time_test {
|
||||||
for (raw, expected) in [
|
for (raw, expected) in [
|
||||||
(
|
(
|
||||||
"2014-11-28T21:00:09+09:00",
|
"2014-11-28T21:00:09+09:00",
|
||||||
DateTime::<FixedOffset>::from_utc(
|
DateTime::<FixedOffset>::from_naive_utc_and_offset(
|
||||||
NaiveDateTime::new(
|
NaiveDateTime::new(
|
||||||
NaiveDate::from_ymd(2014, 11, 28),
|
NaiveDate::from_ymd_opt(2014, 11, 28).unwrap(),
|
||||||
NaiveTime::from_hms(12, 0, 9),
|
NaiveTime::from_hms_opt(12, 0, 9).unwrap(),
|
||||||
),
|
),
|
||||||
FixedOffset::east(9 * 3600),
|
FixedOffset::east_opt(9 * 3600).unwrap(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"2014-11-28T21:00:09Z",
|
"2014-11-28T21:00:09Z",
|
||||||
DateTime::<FixedOffset>::from_utc(
|
DateTime::<FixedOffset>::from_naive_utc_and_offset(
|
||||||
NaiveDateTime::new(
|
NaiveDateTime::new(
|
||||||
NaiveDate::from_ymd(2014, 11, 28),
|
NaiveDate::from_ymd_opt(2014, 11, 28).unwrap(),
|
||||||
NaiveTime::from_hms(21, 0, 9),
|
NaiveTime::from_hms_opt(21, 0, 9).unwrap(),
|
||||||
),
|
),
|
||||||
FixedOffset::east(0),
|
FixedOffset::east_opt(0).unwrap(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2014-11-28 21:00:09z",
|
||||||
|
DateTime::<FixedOffset>::from_naive_utc_and_offset(
|
||||||
|
NaiveDateTime::new(
|
||||||
|
NaiveDate::from_ymd_opt(2014, 11, 28).unwrap(),
|
||||||
|
NaiveTime::from_hms_opt(21, 0, 9).unwrap(),
|
||||||
|
),
|
||||||
|
FixedOffset::east_opt(0).unwrap(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"2014-11-28T21:00:09+00:00",
|
"2014-11-28T21:00:09+00:00",
|
||||||
DateTime::<FixedOffset>::from_utc(
|
DateTime::<FixedOffset>::from_naive_utc_and_offset(
|
||||||
NaiveDateTime::new(
|
NaiveDateTime::new(
|
||||||
NaiveDate::from_ymd(2014, 11, 28),
|
NaiveDate::from_ymd_opt(2014, 11, 28).unwrap(),
|
||||||
NaiveTime::from_hms(21, 0, 9),
|
NaiveTime::from_hms_opt(21, 0, 9).unwrap(),
|
||||||
),
|
),
|
||||||
FixedOffset::east(0),
|
FixedOffset::east_opt(0).unwrap(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"2014-11-28T21:00:09.05+09:00",
|
"2014-11-28T21:00:09.05+09:00",
|
||||||
DateTime::<FixedOffset>::from_utc(
|
DateTime::<FixedOffset>::from_naive_utc_and_offset(
|
||||||
NaiveDateTime::new(
|
NaiveDateTime::new(
|
||||||
NaiveDate::from_ymd(2014, 11, 28),
|
NaiveDate::from_ymd_opt(2014, 11, 28).unwrap(),
|
||||||
NaiveTime::from_hms_milli(12, 0, 9, 50),
|
NaiveTime::from_hms_milli_opt(12, 0, 9, 50).unwrap(),
|
||||||
),
|
),
|
||||||
FixedOffset::east(0),
|
FixedOffset::east_opt(0).unwrap(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"2014-11-28 21:00:09.05+09:00",
|
||||||
|
DateTime::<FixedOffset>::from_naive_utc_and_offset(
|
||||||
|
NaiveDateTime::new(
|
||||||
|
NaiveDate::from_ymd_opt(2014, 11, 28).unwrap(),
|
||||||
|
NaiveTime::from_hms_milli_opt(12, 0, 9, 50).unwrap(),
|
||||||
|
),
|
||||||
|
FixedOffset::east_opt(0).unwrap(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
|
@ -647,7 +686,7 @@ mod date_time_test {
|
||||||
graphql_input_value!("56:34:22"),
|
graphql_input_value!("56:34:22"),
|
||||||
graphql_input_value!("56:34:22.000"),
|
graphql_input_value!("56:34:22.000"),
|
||||||
graphql_input_value!("1996-12-1914:23:43"),
|
graphql_input_value!("1996-12-1914:23:43"),
|
||||||
graphql_input_value!("1996-12-19 14:23:43Z"),
|
graphql_input_value!("1996-12-19Q14:23:43Z"),
|
||||||
graphql_input_value!("1996-12-19T14:23:43"),
|
graphql_input_value!("1996-12-19T14:23:43"),
|
||||||
graphql_input_value!("1996-12-19T14:23:43ZZ"),
|
graphql_input_value!("1996-12-19T14:23:43ZZ"),
|
||||||
graphql_input_value!("1996-12-19T14:23:43.543"),
|
graphql_input_value!("1996-12-19T14:23:43.543"),
|
||||||
|
@ -677,22 +716,22 @@ mod date_time_test {
|
||||||
fn formats_correctly() {
|
fn formats_correctly() {
|
||||||
for (val, expected) in [
|
for (val, expected) in [
|
||||||
(
|
(
|
||||||
DateTime::<FixedOffset>::from_utc(
|
DateTime::<FixedOffset>::from_naive_utc_and_offset(
|
||||||
NaiveDateTime::new(
|
NaiveDateTime::new(
|
||||||
NaiveDate::from_ymd(1996, 12, 19),
|
NaiveDate::from_ymd_opt(1996, 12, 19).unwrap(),
|
||||||
NaiveTime::from_hms(0, 0, 0),
|
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
|
||||||
),
|
),
|
||||||
FixedOffset::east(0),
|
FixedOffset::east_opt(0).unwrap(),
|
||||||
),
|
),
|
||||||
graphql_input_value!("1996-12-19T00:00:00Z"),
|
graphql_input_value!("1996-12-19T00:00:00Z"),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
DateTime::<FixedOffset>::from_utc(
|
DateTime::<FixedOffset>::from_naive_utc_and_offset(
|
||||||
NaiveDateTime::new(
|
NaiveDateTime::new(
|
||||||
NaiveDate::from_ymd(1564, 1, 30),
|
NaiveDate::from_ymd_opt(1564, 1, 30).unwrap(),
|
||||||
NaiveTime::from_hms_milli(5, 0, 0, 123),
|
NaiveTime::from_hms_milli_opt(5, 0, 0, 123).unwrap(),
|
||||||
),
|
),
|
||||||
FixedOffset::east(9 * 3600),
|
FixedOffset::east_opt(9 * 3600).unwrap(),
|
||||||
),
|
),
|
||||||
graphql_input_value!("1564-01-30T05:00:00.123Z"),
|
graphql_input_value!("1564-01-30T05:00:00.123Z"),
|
||||||
),
|
),
|
||||||
|
@ -712,7 +751,9 @@ mod integration_test {
|
||||||
types::scalars::{EmptyMutation, EmptySubscription},
|
types::scalars::{EmptyMutation, EmptySubscription},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Date, DateTime, FixedOffset, FromFixedOffset, LocalDateTime, LocalTime, TimeZone};
|
use super::{
|
||||||
|
DateTime, FixedOffset, FromFixedOffset, LocalDate, LocalDateTime, LocalTime, TimeZone,
|
||||||
|
};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn serializes() {
|
async fn serializes() {
|
||||||
|
@ -759,23 +800,26 @@ mod integration_test {
|
||||||
|
|
||||||
#[graphql_object]
|
#[graphql_object]
|
||||||
impl Root {
|
impl Root {
|
||||||
fn date() -> Date {
|
fn local_date() -> LocalDate {
|
||||||
Date::from_ymd(2015, 3, 14)
|
LocalDate::from_ymd_opt(2015, 3, 14).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_time() -> LocalTime {
|
fn local_time() -> LocalTime {
|
||||||
LocalTime::from_hms(16, 7, 8)
|
LocalTime::from_hms_opt(16, 7, 8).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_date_time() -> LocalDateTime {
|
fn local_date_time() -> LocalDateTime {
|
||||||
LocalDateTime::new(Date::from_ymd(2016, 7, 8), LocalTime::from_hms(9, 10, 11))
|
LocalDateTime::new(
|
||||||
|
LocalDate::from_ymd_opt(2016, 7, 8).unwrap(),
|
||||||
|
LocalTime::from_hms_opt(9, 10, 11).unwrap(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn date_time() -> DateTime<chrono::Utc> {
|
fn date_time() -> DateTime<chrono::Utc> {
|
||||||
DateTime::from_utc(
|
DateTime::from_naive_utc_and_offset(
|
||||||
LocalDateTime::new(
|
LocalDateTime::new(
|
||||||
Date::from_ymd(1996, 12, 20),
|
LocalDate::from_ymd_opt(1996, 12, 20).unwrap(),
|
||||||
LocalTime::from_hms(0, 39, 57),
|
LocalTime::from_hms_opt(0, 39, 57).unwrap(),
|
||||||
),
|
),
|
||||||
chrono::Utc,
|
chrono::Utc,
|
||||||
)
|
)
|
||||||
|
@ -791,7 +835,7 @@ mod integration_test {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DOC: &str = r#"{
|
const DOC: &str = r#"{
|
||||||
date
|
localDate
|
||||||
localTime
|
localTime
|
||||||
localDateTime
|
localDateTime
|
||||||
dateTime,
|
dateTime,
|
||||||
|
@ -809,9 +853,9 @@ mod integration_test {
|
||||||
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
|
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
|
||||||
Ok((
|
Ok((
|
||||||
graphql_value!({
|
graphql_value!({
|
||||||
"date": "2015-03-14",
|
"localDate": "2015-03-14",
|
||||||
"localTime": "16:07:08",
|
"localTime": "16:07:08",
|
||||||
"localDateTime": "2016-07-08 09:10:11",
|
"localDateTime": "2016-07-08T09:10:11",
|
||||||
"dateTime": "1996-12-20T00:39:57Z",
|
"dateTime": "1996-12-20T00:39:57Z",
|
||||||
"passDateTime": "2014-11-28T12:00:09Z",
|
"passDateTime": "2014-11-28T12:00:09Z",
|
||||||
"transformDateTime": "2014-11-28T12:00:09Z",
|
"transformDateTime": "2014-11-28T12:00:09Z",
|
||||||
|
|
|
@ -2,27 +2,35 @@
|
||||||
//!
|
//!
|
||||||
//! # Supported types
|
//! # Supported types
|
||||||
//!
|
//!
|
||||||
//! | Rust type | Format | GraphQL scalar |
|
//! | Rust type | Format | GraphQL scalar |
|
||||||
//! |-----------|--------------------|----------------|
|
//! |-----------|--------------------|------------------|
|
||||||
//! | [`Tz`] | [IANA database][1] | `TimeZone` |
|
//! | [`Tz`] | [IANA database][1] | [`TimeZone`][s1] |
|
||||||
//!
|
//!
|
||||||
//! [`chrono-tz`]: chrono_tz
|
//! [`chrono-tz`]: chrono_tz
|
||||||
//! [`Tz`]: chrono_tz::Tz
|
//! [`Tz`]: chrono_tz::Tz
|
||||||
//! [1]: http://www.iana.org/time-zones
|
//! [1]: http://www.iana.org/time-zones
|
||||||
|
//! [s1]: https://graphql-scalars.dev/docs/scalars/time-zone
|
||||||
|
|
||||||
use crate::{graphql_scalar, InputValue, ScalarValue, Value};
|
use crate::{graphql_scalar, InputValue, ScalarValue, Value};
|
||||||
|
|
||||||
/// Timezone based on [`IANA` database][1].
|
/// Timezone based on [`IANA` database][0].
|
||||||
///
|
///
|
||||||
/// See ["List of tz database time zones"][2] `TZ database name` column for
|
/// See ["List of tz database time zones"][3] `TZ database name` column for
|
||||||
/// available names.
|
/// available names.
|
||||||
///
|
///
|
||||||
/// See also [`chrono_tz::Tz`][3] for detals.
|
/// [`TimeZone` scalar][1] compliant.
|
||||||
///
|
///
|
||||||
/// [1]: https://www.iana.org/time-zones
|
/// See also [`chrono_tz::Tz`][2] for details.
|
||||||
/// [2]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
///
|
||||||
/// [3]: https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html
|
/// [0]: https://www.iana.org/time-zones
|
||||||
#[graphql_scalar(with = tz, parse_token(String))]
|
/// [1]: https://graphql-scalars.dev/docs/scalars/time-zone
|
||||||
|
/// [2]: https://docs.rs/chrono-tz/*/chrono_tz/enum.Tz.html
|
||||||
|
/// [3]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||||
|
#[graphql_scalar(
|
||||||
|
with = tz,
|
||||||
|
parse_token(String),
|
||||||
|
specified_by_url = "https://graphql-scalars.dev/docs/scalars/time-zone",
|
||||||
|
)]
|
||||||
pub type TimeZone = chrono_tz::Tz;
|
pub type TimeZone = chrono_tz::Tz;
|
||||||
|
|
||||||
mod tz {
|
mod tz {
|
||||||
|
@ -78,7 +86,7 @@ mod test {
|
||||||
fn forward_slash() {
|
fn forward_slash() {
|
||||||
tz_input_test(
|
tz_input_test(
|
||||||
"Abc/Xyz",
|
"Abc/Xyz",
|
||||||
Err("Failed to parse `TimeZone`: received invalid timezone"),
|
Err("Failed to parse `TimeZone`: failed to parse timezone"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +94,7 @@ mod test {
|
||||||
fn number() {
|
fn number() {
|
||||||
tz_input_test(
|
tz_input_test(
|
||||||
"8086",
|
"8086",
|
||||||
Err("Failed to parse `TimeZone`: received invalid timezone"),
|
Err("Failed to parse `TimeZone`: failed to parse timezone"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +102,7 @@ mod test {
|
||||||
fn no_forward_slash() {
|
fn no_forward_slash() {
|
||||||
tz_input_test(
|
tz_input_test(
|
||||||
"AbcXyz",
|
"AbcXyz",
|
||||||
Err("Failed to parse `TimeZone`: received invalid timezone"),
|
Err("Failed to parse `TimeZone`: failed to parse timezone"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1683
juniper/src/integrations/jiff.rs
Normal file
1683
juniper/src/integrations/jiff.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,7 @@
|
||||||
//! Provides GraphQLType implementations for some external types
|
//! Provides GraphQLType implementations for some external types
|
||||||
|
|
||||||
|
#[cfg(feature = "anyhow")]
|
||||||
|
pub mod anyhow;
|
||||||
#[cfg(feature = "bigdecimal")]
|
#[cfg(feature = "bigdecimal")]
|
||||||
pub mod bigdecimal;
|
pub mod bigdecimal;
|
||||||
#[cfg(feature = "bson")]
|
#[cfg(feature = "bson")]
|
||||||
|
@ -8,6 +10,8 @@ pub mod bson;
|
||||||
pub mod chrono;
|
pub mod chrono;
|
||||||
#[cfg(feature = "chrono-tz")]
|
#[cfg(feature = "chrono-tz")]
|
||||||
pub mod chrono_tz;
|
pub mod chrono_tz;
|
||||||
|
#[cfg(feature = "jiff")]
|
||||||
|
pub mod jiff;
|
||||||
#[cfg(feature = "rust_decimal")]
|
#[cfg(feature = "rust_decimal")]
|
||||||
pub mod rust_decimal;
|
pub mod rust_decimal;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
|
|
@ -252,8 +252,8 @@ impl Serialize for Spanning<ParseError> {
|
||||||
map.serialize_value(&msg)?;
|
map.serialize_value(&msg)?;
|
||||||
|
|
||||||
let mut loc = IndexMap::new();
|
let mut loc = IndexMap::new();
|
||||||
loc.insert("line".to_owned(), self.start.line() + 1);
|
loc.insert("line".to_owned(), self.start().line() + 1);
|
||||||
loc.insert("column".to_owned(), self.start.column() + 1);
|
loc.insert("column".to_owned(), self.start().column() + 1);
|
||||||
|
|
||||||
let locations = vec![loc];
|
let locations = vec![loc];
|
||||||
map.serialize_key("locations")?;
|
map.serialize_key("locations")?;
|
||||||
|
@ -289,7 +289,7 @@ impl<'de> Deserialize<'de> for DefaultScalarValue {
|
||||||
fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
|
fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
|
||||||
struct Visitor;
|
struct Visitor;
|
||||||
|
|
||||||
impl<'de> de::Visitor<'de> for Visitor {
|
impl de::Visitor<'_> for Visitor {
|
||||||
type Value = DefaultScalarValue;
|
type Value = DefaultScalarValue;
|
||||||
|
|
||||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
//!
|
//!
|
||||||
//! # Supported types
|
//! # Supported types
|
||||||
//!
|
//!
|
||||||
//! | Rust type | Format | GraphQL scalar |
|
//! | Rust type | Format | GraphQL scalar |
|
||||||
//! |-----------------------|-----------------------|---------------------|
|
//! |-----------------------|-----------------------|-----------------------|
|
||||||
//! | [`Date`] | `yyyy-MM-dd` | [`Date`][s1] |
|
//! | [`Date`] | `yyyy-MM-dd` | [`LocalDate`][s1] |
|
||||||
//! | [`Time`] | `HH:mm[:ss[.SSS]]` | [`LocalTime`][s2] |
|
//! | [`Time`] | `HH:mm[:ss[.SSS]]` | [`LocalTime`][s2] |
|
||||||
//! | [`PrimitiveDateTime`] | `yyyy-MM-dd HH:mm:ss` | `LocalDateTime` |
|
//! | [`PrimitiveDateTime`] | `yyyy-MM-ddTHH:mm:ss` | [`LocalDateTime`][s3] |
|
||||||
//! | [`OffsetDateTime`] | [RFC 3339] string | [`DateTime`][s4] |
|
//! | [`OffsetDateTime`] | [RFC 3339] string | [`DateTime`][s4] |
|
||||||
//! | [`UtcOffset`] | `±hh:mm` | [`UtcOffset`][s5] |
|
//! | [`UtcOffset`] | `±hh:mm` | [`UtcOffset`][s5] |
|
||||||
//!
|
//!
|
||||||
//! [`Date`]: time::Date
|
//! [`Date`]: time::Date
|
||||||
//! [`OffsetDateTime`]: time::OffsetDateTime
|
//! [`OffsetDateTime`]: time::OffsetDateTime
|
||||||
|
@ -16,13 +16,14 @@
|
||||||
//! [`Time`]: time::Time
|
//! [`Time`]: time::Time
|
||||||
//! [`UtcOffset`]: time::UtcOffset
|
//! [`UtcOffset`]: time::UtcOffset
|
||||||
//! [RFC 3339]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
|
//! [RFC 3339]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6
|
||||||
//! [s1]: https://graphql-scalars.dev/docs/scalars/date
|
//! [s1]: https://graphql-scalars.dev/docs/scalars/local-date
|
||||||
//! [s2]: https://graphql-scalars.dev/docs/scalars/local-time
|
//! [s2]: https://graphql-scalars.dev/docs/scalars/local-time
|
||||||
|
//! [s3]: https://graphql-scalars.dev/docs/scalars/local-date-time
|
||||||
//! [s4]: https://graphql-scalars.dev/docs/scalars/date-time
|
//! [s4]: https://graphql-scalars.dev/docs/scalars/date-time
|
||||||
//! [s5]: https://graphql-scalars.dev/docs/scalars/utc-offset
|
//! [s5]: https://graphql-scalars.dev/docs/scalars/utc-offset
|
||||||
|
|
||||||
use time::{
|
use time::{
|
||||||
format_description::{well_known::Rfc3339, FormatItem},
|
format_description::{well_known::Rfc3339, BorrowedFormatItem},
|
||||||
macros::format_description,
|
macros::format_description,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -33,38 +34,40 @@ use crate::{graphql_scalar, InputValue, ScalarValue, Value};
|
||||||
/// Represents a description of the date (as used for birthdays, for example).
|
/// Represents a description of the date (as used for birthdays, for example).
|
||||||
/// It cannot represent an instant on the time-line.
|
/// It cannot represent an instant on the time-line.
|
||||||
///
|
///
|
||||||
/// [`Date` scalar][1] compliant.
|
/// [`LocalDate` scalar][1] compliant.
|
||||||
///
|
///
|
||||||
/// See also [`time::Date`][2] for details.
|
/// See also [`time::Date`][2] for details.
|
||||||
///
|
///
|
||||||
/// [1]: https://graphql-scalars.dev/docs/scalars/date
|
/// [1]: https://graphql-scalars.dev/docs/scalars/local-date
|
||||||
/// [2]: https://docs.rs/time/*/time/struct.Date.html
|
/// [2]: https://docs.rs/time/*/time/struct.Date.html
|
||||||
#[graphql_scalar(
|
#[graphql_scalar(
|
||||||
with = date,
|
with = local_date,
|
||||||
parse_token(String),
|
parse_token(String),
|
||||||
specified_by_url = "https://graphql-scalars.dev/docs/scalars/date",
|
specified_by_url = "https://graphql-scalars.dev/docs/scalars/local-date",
|
||||||
)]
|
)]
|
||||||
pub type Date = time::Date;
|
pub type LocalDate = time::Date;
|
||||||
|
|
||||||
mod date {
|
mod local_date {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Format of a [`Date` scalar][1].
|
/// Format of a [`LocalDate` scalar][1].
|
||||||
///
|
///
|
||||||
/// [1]: https://graphql-scalars.dev/docs/scalars/date
|
/// [1]: https://graphql-scalars.dev/docs/scalars/local-date
|
||||||
const FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day]");
|
const FORMAT: &[BorrowedFormatItem<'_>] = format_description!("[year]-[month]-[day]");
|
||||||
|
|
||||||
pub(super) fn to_output<S: ScalarValue>(v: &Date) -> Value<S> {
|
pub(super) fn to_output<S: ScalarValue>(v: &LocalDate) -> Value<S> {
|
||||||
Value::scalar(
|
Value::scalar(
|
||||||
v.format(FORMAT)
|
v.format(FORMAT)
|
||||||
.unwrap_or_else(|e| panic!("Failed to format `Date`: {e}")),
|
.unwrap_or_else(|e| panic!("failed to format `LocalDate`: {e}")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Date, String> {
|
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<LocalDate, String> {
|
||||||
v.as_string_value()
|
v.as_string_value()
|
||||||
.ok_or_else(|| format!("Expected `String`, found: {v}"))
|
.ok_or_else(|| format!("Expected `String`, found: {v}"))
|
||||||
.and_then(|s| Date::parse(s, FORMAT).map_err(|e| format!("Invalid `Date`: {e}")))
|
.and_then(|s| {
|
||||||
|
LocalDate::parse(s, FORMAT).map_err(|e| format!("Invalid `LocalDate`: {e}"))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +83,11 @@ mod date {
|
||||||
///
|
///
|
||||||
/// [1]: https://graphql-scalars.dev/docs/scalars/local-time
|
/// [1]: https://graphql-scalars.dev/docs/scalars/local-time
|
||||||
/// [2]: https://docs.rs/time/*/time/struct.Time.html
|
/// [2]: https://docs.rs/time/*/time/struct.Time.html
|
||||||
#[graphql_scalar(with = local_time, parse_token(String))]
|
#[graphql_scalar(
|
||||||
|
with = local_time,
|
||||||
|
parse_token(String),
|
||||||
|
specified_by_url = "https://graphql-scalars.dev/docs/scalars/local-time",
|
||||||
|
)]
|
||||||
pub type LocalTime = time::Time;
|
pub type LocalTime = time::Time;
|
||||||
|
|
||||||
mod local_time {
|
mod local_time {
|
||||||
|
@ -89,18 +96,19 @@ mod local_time {
|
||||||
/// Full format of a [`LocalTime` scalar][1].
|
/// Full format of a [`LocalTime` scalar][1].
|
||||||
///
|
///
|
||||||
/// [1]: https://graphql-scalars.dev/docs/scalars/local-time
|
/// [1]: https://graphql-scalars.dev/docs/scalars/local-time
|
||||||
const FORMAT: &[FormatItem<'_>] =
|
const FORMAT: &[BorrowedFormatItem<'_>] =
|
||||||
format_description!("[hour]:[minute]:[second].[subsecond digits:3]");
|
format_description!("[hour]:[minute]:[second].[subsecond digits:3]");
|
||||||
|
|
||||||
/// Format of a [`LocalTime` scalar][1] without milliseconds.
|
/// Format of a [`LocalTime` scalar][1] without milliseconds.
|
||||||
///
|
///
|
||||||
/// [1]: https://graphql-scalars.dev/docs/scalars/local-time
|
/// [1]: https://graphql-scalars.dev/docs/scalars/local-time
|
||||||
const FORMAT_NO_MILLIS: &[FormatItem<'_>] = format_description!("[hour]:[minute]:[second]");
|
const FORMAT_NO_MILLIS: &[BorrowedFormatItem<'_>] =
|
||||||
|
format_description!("[hour]:[minute]:[second]");
|
||||||
|
|
||||||
/// Format of a [`LocalTime` scalar][1] without seconds.
|
/// Format of a [`LocalTime` scalar][1] without seconds.
|
||||||
///
|
///
|
||||||
/// [1]: https://graphql-scalars.dev/docs/scalars/local-time
|
/// [1]: https://graphql-scalars.dev/docs/scalars/local-time
|
||||||
const FORMAT_NO_SECS: &[FormatItem<'_>] = format_description!("[hour]:[minute]");
|
const FORMAT_NO_SECS: &[BorrowedFormatItem<'_>] = format_description!("[hour]:[minute]");
|
||||||
|
|
||||||
pub(super) fn to_output<S: ScalarValue>(v: &LocalTime) -> Value<S> {
|
pub(super) fn to_output<S: ScalarValue>(v: &LocalTime) -> Value<S> {
|
||||||
Value::scalar(
|
Value::scalar(
|
||||||
|
@ -109,7 +117,7 @@ mod local_time {
|
||||||
} else {
|
} else {
|
||||||
v.format(FORMAT)
|
v.format(FORMAT)
|
||||||
}
|
}
|
||||||
.unwrap_or_else(|e| panic!("Failed to format `LocalTime`: {e}")),
|
.unwrap_or_else(|e| panic!("failed to format `LocalTime`: {e}")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,25 +136,34 @@ mod local_time {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Combined date and time (without time zone) in `yyyy-MM-dd HH:mm:ss` format.
|
/// Combined date and time (without time zone) in `yyyy-MM-ddTHH:mm:ss` format.
|
||||||
|
///
|
||||||
|
/// [`LocalDateTime` scalar][1] compliant.
|
||||||
///
|
///
|
||||||
/// See also [`time::PrimitiveDateTime`][2] for details.
|
/// See also [`time::PrimitiveDateTime`][2] for details.
|
||||||
///
|
///
|
||||||
|
/// [1]: https://graphql-scalars.dev/docs/scalars/local-date-time
|
||||||
/// [2]: https://docs.rs/time/*/time/struct.PrimitiveDateTime.html
|
/// [2]: https://docs.rs/time/*/time/struct.PrimitiveDateTime.html
|
||||||
#[graphql_scalar(with = local_date_time, parse_token(String))]
|
#[graphql_scalar(
|
||||||
|
with = local_date_time,
|
||||||
|
parse_token(String),
|
||||||
|
specified_by_url = "https://graphql-scalars.dev/docs/scalars/local-date-time",
|
||||||
|
)]
|
||||||
pub type LocalDateTime = time::PrimitiveDateTime;
|
pub type LocalDateTime = time::PrimitiveDateTime;
|
||||||
|
|
||||||
mod local_date_time {
|
mod local_date_time {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Format of a [`LocalDateTime`] scalar.
|
/// Format of a [`LocalDateTime` scalar][1].
|
||||||
const FORMAT: &[FormatItem<'_>] =
|
///
|
||||||
format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
|
/// [1]: https://graphql-scalars.dev/docs/scalars/local-date-time
|
||||||
|
const FORMAT: &[BorrowedFormatItem<'_>] =
|
||||||
|
format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]");
|
||||||
|
|
||||||
pub(super) fn to_output<S: ScalarValue>(v: &LocalDateTime) -> Value<S> {
|
pub(super) fn to_output<S: ScalarValue>(v: &LocalDateTime) -> Value<S> {
|
||||||
Value::scalar(
|
Value::scalar(
|
||||||
v.format(FORMAT)
|
v.format(FORMAT)
|
||||||
.unwrap_or_else(|e| panic!("Failed to format `LocalDateTime`: {e}")),
|
.unwrap_or_else(|e| panic!("failed to format `LocalDateTime`: {e}")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,7 +202,7 @@ mod date_time {
|
||||||
Value::scalar(
|
Value::scalar(
|
||||||
v.to_offset(UtcOffset::UTC)
|
v.to_offset(UtcOffset::UTC)
|
||||||
.format(&Rfc3339)
|
.format(&Rfc3339)
|
||||||
.unwrap_or_else(|e| panic!("Failed to format `DateTime`: {e}")),
|
.unwrap_or_else(|e| panic!("failed to format `DateTime`: {e}")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +219,7 @@ mod date_time {
|
||||||
/// Format of a [`UtcOffset` scalar][1].
|
/// Format of a [`UtcOffset` scalar][1].
|
||||||
///
|
///
|
||||||
/// [1]: https://graphql-scalars.dev/docs/scalars/utc-offset
|
/// [1]: https://graphql-scalars.dev/docs/scalars/utc-offset
|
||||||
const UTC_OFFSET_FORMAT: &[FormatItem<'_>] =
|
const UTC_OFFSET_FORMAT: &[BorrowedFormatItem<'_>] =
|
||||||
format_description!("[offset_hour sign:mandatory]:[offset_minute]");
|
format_description!("[offset_hour sign:mandatory]:[offset_minute]");
|
||||||
|
|
||||||
/// Offset from UTC in `±hh:mm` format. See [list of database time zones][0].
|
/// Offset from UTC in `±hh:mm` format. See [list of database time zones][0].
|
||||||
|
@ -227,7 +244,7 @@ mod utc_offset {
|
||||||
pub(super) fn to_output<S: ScalarValue>(v: &UtcOffset) -> Value<S> {
|
pub(super) fn to_output<S: ScalarValue>(v: &UtcOffset) -> Value<S> {
|
||||||
Value::scalar(
|
Value::scalar(
|
||||||
v.format(UTC_OFFSET_FORMAT)
|
v.format(UTC_OFFSET_FORMAT)
|
||||||
.unwrap_or_else(|e| panic!("Failed to format `UtcOffset`: {e}")),
|
.unwrap_or_else(|e| panic!("failed to format `UtcOffset`: {e}")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,12 +259,12 @@ mod utc_offset {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod date_test {
|
mod local_date_test {
|
||||||
use time::macros::date;
|
use time::macros::date;
|
||||||
|
|
||||||
use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
|
use crate::{graphql_input_value, FromInputValue as _, InputValue, ToInputValue as _};
|
||||||
|
|
||||||
use super::Date;
|
use super::LocalDate;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_correct_input() {
|
fn parses_correct_input() {
|
||||||
|
@ -256,7 +273,7 @@ mod date_test {
|
||||||
("1564-01-30", date!(1564 - 01 - 30)),
|
("1564-01-30", date!(1564 - 01 - 30)),
|
||||||
] {
|
] {
|
||||||
let input: InputValue = graphql_input_value!((raw));
|
let input: InputValue = graphql_input_value!((raw));
|
||||||
let parsed = Date::from_input_value(&input);
|
let parsed = LocalDate::from_input_value(&input);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
parsed.is_ok(),
|
parsed.is_ok(),
|
||||||
|
@ -284,7 +301,7 @@ mod date_test {
|
||||||
graphql_input_value!(false),
|
graphql_input_value!(false),
|
||||||
] {
|
] {
|
||||||
let input: InputValue = input;
|
let input: InputValue = input;
|
||||||
let parsed = Date::from_input_value(&input);
|
let parsed = LocalDate::from_input_value(&input);
|
||||||
|
|
||||||
assert!(parsed.is_err(), "allows input: {input:?}");
|
assert!(parsed.is_err(), "allows input: {input:?}");
|
||||||
}
|
}
|
||||||
|
@ -391,8 +408,8 @@ mod local_date_time_test {
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_correct_input() {
|
fn parses_correct_input() {
|
||||||
for (raw, expected) in [
|
for (raw, expected) in [
|
||||||
("1996-12-19 14:23:43", datetime!(1996-12-19 14:23:43)),
|
("1996-12-19T14:23:43", datetime!(1996-12-19 14:23:43)),
|
||||||
("1564-01-30 14:00:00", datetime!(1564-01-30 14:00)),
|
("1564-01-30T14:00:00", datetime!(1564-01-30 14:00)),
|
||||||
] {
|
] {
|
||||||
let input: InputValue = graphql_input_value!((raw));
|
let input: InputValue = graphql_input_value!((raw));
|
||||||
let parsed = LocalDateTime::from_input_value(&input);
|
let parsed = LocalDateTime::from_input_value(&input);
|
||||||
|
@ -414,16 +431,17 @@ mod local_date_time_test {
|
||||||
graphql_input_value!("56:34:22"),
|
graphql_input_value!("56:34:22"),
|
||||||
graphql_input_value!("56:34:22.000"),
|
graphql_input_value!("56:34:22.000"),
|
||||||
graphql_input_value!("1996-12-1914:23:43"),
|
graphql_input_value!("1996-12-1914:23:43"),
|
||||||
graphql_input_value!("1996-12-19T14:23:43"),
|
graphql_input_value!("1996-12-19 14:23:43"),
|
||||||
graphql_input_value!("1996-12-19 14:23:43Z"),
|
graphql_input_value!("1996-12-19Q14:23:43"),
|
||||||
graphql_input_value!("1996-12-19 14:23:43.543"),
|
graphql_input_value!("1996-12-19T14:23:43Z"),
|
||||||
graphql_input_value!("1996-12-19 14:23"),
|
graphql_input_value!("1996-12-19T14:23:43.543"),
|
||||||
graphql_input_value!("1996-12-19 14:23:1"),
|
graphql_input_value!("1996-12-19T14:23"),
|
||||||
graphql_input_value!("1996-12-19 14:23:"),
|
graphql_input_value!("1996-12-19T14:23:1"),
|
||||||
graphql_input_value!("1996-12-19 23:78:43"),
|
graphql_input_value!("1996-12-19T14:23:"),
|
||||||
graphql_input_value!("1996-12-19 23:18:99"),
|
graphql_input_value!("1996-12-19T23:78:43"),
|
||||||
graphql_input_value!("1996-12-19 24:00:00"),
|
graphql_input_value!("1996-12-19T23:18:99"),
|
||||||
graphql_input_value!("1996-12-19 99:02:13"),
|
graphql_input_value!("1996-12-19T24:00:00"),
|
||||||
|
graphql_input_value!("1996-12-19T99:02:13"),
|
||||||
graphql_input_value!("i'm not even a datetime"),
|
graphql_input_value!("i'm not even a datetime"),
|
||||||
graphql_input_value!(2.32),
|
graphql_input_value!(2.32),
|
||||||
graphql_input_value!(1),
|
graphql_input_value!(1),
|
||||||
|
@ -442,11 +460,11 @@ mod local_date_time_test {
|
||||||
for (val, expected) in [
|
for (val, expected) in [
|
||||||
(
|
(
|
||||||
datetime!(1996-12-19 12:00 am),
|
datetime!(1996-12-19 12:00 am),
|
||||||
graphql_input_value!("1996-12-19 00:00:00"),
|
graphql_input_value!("1996-12-19T00:00:00"),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
datetime!(1564-01-30 14:00),
|
datetime!(1564-01-30 14:00),
|
||||||
graphql_input_value!("1564-01-30 14:00:00"),
|
graphql_input_value!("1564-01-30T14:00:00"),
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
let actual: InputValue = val.to_input_value();
|
let actual: InputValue = val.to_input_value();
|
||||||
|
@ -472,6 +490,7 @@ mod date_time_test {
|
||||||
datetime!(2014-11-28 21:00:09 +9),
|
datetime!(2014-11-28 21:00:09 +9),
|
||||||
),
|
),
|
||||||
("2014-11-28T21:00:09Z", datetime!(2014-11-28 21:00:09 +0)),
|
("2014-11-28T21:00:09Z", datetime!(2014-11-28 21:00:09 +0)),
|
||||||
|
("2014-11-28 21:00:09z", datetime!(2014-11-28 21:00:09 +0)),
|
||||||
(
|
(
|
||||||
"2014-11-28T21:00:09+00:00",
|
"2014-11-28T21:00:09+00:00",
|
||||||
datetime!(2014-11-28 21:00:09 +0),
|
datetime!(2014-11-28 21:00:09 +0),
|
||||||
|
@ -480,6 +499,10 @@ mod date_time_test {
|
||||||
"2014-11-28T21:00:09.05+09:00",
|
"2014-11-28T21:00:09.05+09:00",
|
||||||
datetime!(2014-11-28 12:00:09.05 +0),
|
datetime!(2014-11-28 12:00:09.05 +0),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"2014-11-28 21:00:09.05+09:00",
|
||||||
|
datetime!(2014-11-28 12:00:09.05 +0),
|
||||||
|
),
|
||||||
] {
|
] {
|
||||||
let input: InputValue = graphql_input_value!((raw));
|
let input: InputValue = graphql_input_value!((raw));
|
||||||
let parsed = DateTime::from_input_value(&input);
|
let parsed = DateTime::from_input_value(&input);
|
||||||
|
@ -501,7 +524,6 @@ mod date_time_test {
|
||||||
graphql_input_value!("56:34:22"),
|
graphql_input_value!("56:34:22"),
|
||||||
graphql_input_value!("56:34:22.000"),
|
graphql_input_value!("56:34:22.000"),
|
||||||
graphql_input_value!("1996-12-1914:23:43"),
|
graphql_input_value!("1996-12-1914:23:43"),
|
||||||
graphql_input_value!("1996-12-19 14:23:43Z"),
|
|
||||||
graphql_input_value!("1996-12-19T14:23:43"),
|
graphql_input_value!("1996-12-19T14:23:43"),
|
||||||
graphql_input_value!("1996-12-19T14:23:43ZZ"),
|
graphql_input_value!("1996-12-19T14:23:43ZZ"),
|
||||||
graphql_input_value!("1996-12-19T14:23:43.543"),
|
graphql_input_value!("1996-12-19T14:23:43.543"),
|
||||||
|
@ -629,7 +651,7 @@ mod integration_test {
|
||||||
types::scalars::{EmptyMutation, EmptySubscription},
|
types::scalars::{EmptyMutation, EmptySubscription},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Date, DateTime, LocalDateTime, LocalTime, UtcOffset};
|
use super::{DateTime, LocalDate, LocalDateTime, LocalTime, UtcOffset};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn serializes() {
|
async fn serializes() {
|
||||||
|
@ -637,7 +659,7 @@ mod integration_test {
|
||||||
|
|
||||||
#[graphql_object]
|
#[graphql_object]
|
||||||
impl Root {
|
impl Root {
|
||||||
fn date() -> Date {
|
fn local_date() -> LocalDate {
|
||||||
date!(2015 - 03 - 14)
|
date!(2015 - 03 - 14)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -659,7 +681,7 @@ mod integration_test {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DOC: &str = r#"{
|
const DOC: &str = r#"{
|
||||||
date
|
localDate
|
||||||
localTime
|
localTime
|
||||||
localDateTime
|
localDateTime
|
||||||
dateTime,
|
dateTime,
|
||||||
|
@ -676,9 +698,9 @@ mod integration_test {
|
||||||
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
|
execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
|
||||||
Ok((
|
Ok((
|
||||||
graphql_value!({
|
graphql_value!({
|
||||||
"date": "2015-03-14",
|
"localDate": "2015-03-14",
|
||||||
"localTime": "16:07:08",
|
"localTime": "16:07:08",
|
||||||
"localDateTime": "2016-07-08 09:10:11",
|
"localDateTime": "2016-07-08T09:10:11",
|
||||||
"dateTime": "1996-12-20T00:39:57Z",
|
"dateTime": "1996-12-20T00:39:57Z",
|
||||||
"utcOffset": "+11:30",
|
"utcOffset": "+11:30",
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,8 +1,32 @@
|
||||||
//! GraphQL support for [url](https://github.com/servo/rust-url) types.
|
//! GraphQL support for [`url`] crate types.
|
||||||
|
//!
|
||||||
|
//! # Supported types
|
||||||
|
//!
|
||||||
|
//! | Rust type | GraphQL scalar |
|
||||||
|
//! |-----------|----------------|
|
||||||
|
//! | [`Url`] | [`URL`][s1] |
|
||||||
|
//!
|
||||||
|
//! [`Url`]: url::Url
|
||||||
|
//! [s1]: https://graphql-scalars.dev/docs/scalars/url
|
||||||
|
|
||||||
use crate::{graphql_scalar, InputValue, ScalarValue, Value};
|
use crate::{graphql_scalar, InputValue, ScalarValue, Value};
|
||||||
|
|
||||||
#[graphql_scalar(with = url_scalar, parse_token(String))]
|
/// [Standard URL][0] format as specified in [RFC 3986].
|
||||||
|
///
|
||||||
|
/// [`URL` scalar][1] compliant.
|
||||||
|
///
|
||||||
|
/// See also [`url::Url`][2] for details.
|
||||||
|
///
|
||||||
|
/// [0]: http://url.spec.whatwg.org
|
||||||
|
/// [1]: https://graphql-scalars.dev/docs/scalars/url
|
||||||
|
/// [2]: https://docs.rs/url/*/url/struct.Url.html
|
||||||
|
/// [RFC 3986]: https://datatracker.ietf.org/doc/html/rfc3986
|
||||||
|
#[graphql_scalar(
|
||||||
|
name = "URL",
|
||||||
|
with = url_scalar,
|
||||||
|
parse_token(String),
|
||||||
|
specified_by_url = "https://graphql-scalars.dev/docs/scalars/url",
|
||||||
|
)]
|
||||||
type Url = url::Url;
|
type Url = url::Url;
|
||||||
|
|
||||||
mod url_scalar {
|
mod url_scalar {
|
||||||
|
@ -15,7 +39,7 @@ mod url_scalar {
|
||||||
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Url, String> {
|
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Url, String> {
|
||||||
v.as_string_value()
|
v.as_string_value()
|
||||||
.ok_or_else(|| format!("Expected `String`, found: {v}"))
|
.ok_or_else(|| format!("Expected `String`, found: {v}"))
|
||||||
.and_then(|s| Url::parse(s).map_err(|e| format!("Failed to parse `Url`: {e}")))
|
.and_then(|s| Url::parse(s).map_err(|e| format!("Failed to parse `URL`: {e}")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,31 @@
|
||||||
//! GraphQL support for [uuid](https://doc.rust-lang.org/uuid/uuid/struct.Uuid.html) types.
|
//! GraphQL support for [`uuid`] crate types.
|
||||||
|
//!
|
||||||
#![allow(clippy::needless_lifetimes)]
|
//! # Supported types
|
||||||
|
//!
|
||||||
|
//! | Rust type | GraphQL scalar |
|
||||||
|
//! |-----------|----------------|
|
||||||
|
//! | [`Uuid`] | [`UUID`][s1] |
|
||||||
|
//!
|
||||||
|
//! [`Uuid`]: uuid::Uuid
|
||||||
|
//! [s1]: https://graphql-scalars.dev/docs/scalars/uuid
|
||||||
|
|
||||||
use crate::{graphql_scalar, InputValue, ScalarValue, Value};
|
use crate::{graphql_scalar, InputValue, ScalarValue, Value};
|
||||||
|
|
||||||
#[graphql_scalar(with = uuid_scalar, parse_token(String))]
|
/// [Universally Unique Identifier][0] (UUID).
|
||||||
|
///
|
||||||
|
/// [`UUID` scalar][1] compliant.
|
||||||
|
///
|
||||||
|
/// See also [`uuid::Uuid`][2] for details.
|
||||||
|
///
|
||||||
|
/// [0]: https://en.wikipedia.org/wiki/Universally_unique_identifier
|
||||||
|
/// [1]: https://graphql-scalars.dev/docs/scalars/uuid
|
||||||
|
/// [2]: https://docs.rs/uuid/*/uuid/struct.Uuid.html
|
||||||
|
#[graphql_scalar(
|
||||||
|
name = "UUID",
|
||||||
|
with = uuid_scalar,
|
||||||
|
parse_token(String),
|
||||||
|
specified_by_url = "https://graphql-scalars.dev/docs/scalars/uuid",
|
||||||
|
)]
|
||||||
type Uuid = uuid::Uuid;
|
type Uuid = uuid::Uuid;
|
||||||
|
|
||||||
mod uuid_scalar {
|
mod uuid_scalar {
|
||||||
|
@ -17,7 +38,7 @@ mod uuid_scalar {
|
||||||
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Uuid, String> {
|
pub(super) fn from_input<S: ScalarValue>(v: &InputValue<S>) -> Result<Uuid, String> {
|
||||||
v.as_string_value()
|
v.as_string_value()
|
||||||
.ok_or_else(|| format!("Expected `String`, found: {v}"))
|
.ok_or_else(|| format!("Expected `String`, found: {v}"))
|
||||||
.and_then(|s| Uuid::parse_str(s).map_err(|e| format!("Failed to parse `Uuid`: {e}")))
|
.and_then(|s| Uuid::parse_str(s).map_err(|e| format!("Failed to parse `UUID`: {e}")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,10 @@ use crate::{
|
||||||
executor::{execute_validated_query, get_operation},
|
executor::{execute_validated_query, get_operation},
|
||||||
introspection::{INTROSPECTION_QUERY, INTROSPECTION_QUERY_WITHOUT_DESCRIPTIONS},
|
introspection::{INTROSPECTION_QUERY, INTROSPECTION_QUERY_WITHOUT_DESCRIPTIONS},
|
||||||
parser::parse_document_source,
|
parser::parse_document_source,
|
||||||
validation::{validate_input_values, visit_all_rules, ValidatorContext},
|
validation::{
|
||||||
|
rules, validate_input_values, visit as visit_rule, visit_all_rules, MultiVisitorNil,
|
||||||
|
ValidatorContext,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
|
@ -68,12 +71,13 @@ pub use crate::{
|
||||||
},
|
},
|
||||||
executor::{
|
executor::{
|
||||||
Applies, Context, ExecutionError, ExecutionResult, Executor, FieldError, FieldResult,
|
Applies, Context, ExecutionError, ExecutionResult, Executor, FieldError, FieldResult,
|
||||||
FromContext, IntoFieldError, IntoResolvable, LookAheadArgument, LookAheadMethods,
|
FromContext, IntoFieldError, IntoResolvable, LookAheadArgument, LookAheadChildren,
|
||||||
LookAheadSelection, LookAheadValue, OwnedExecutor, Registry, ValuesStream, Variables,
|
LookAheadList, LookAheadObject, LookAheadSelection, LookAheadValue, OwnedExecutor,
|
||||||
|
Registry, ValuesStream, Variables,
|
||||||
},
|
},
|
||||||
introspection::IntrospectionFormat,
|
introspection::IntrospectionFormat,
|
||||||
macros::helper::subscription::{ExtractTypeFromStream, IntoFieldResult},
|
macros::helper::subscription::{ExtractTypeFromStream, IntoFieldResult},
|
||||||
parser::{ParseError, ScalarToken, Spanning},
|
parser::{ParseError, ScalarToken, Span, Spanning},
|
||||||
schema::{
|
schema::{
|
||||||
meta,
|
meta,
|
||||||
model::{RootNode, SchemaType},
|
model::{RootNode, SchemaType},
|
||||||
|
@ -95,7 +99,7 @@ pub use crate::{
|
||||||
|
|
||||||
/// An error that prevented query execution
|
/// An error that prevented query execution
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum GraphQLError {
|
pub enum GraphQLError {
|
||||||
ParseError(Spanning<ParseError>),
|
ParseError(Spanning<ParseError>),
|
||||||
ValidationError(Vec<RuleError>),
|
ValidationError(Vec<RuleError>),
|
||||||
|
@ -158,6 +162,13 @@ where
|
||||||
{
|
{
|
||||||
let mut ctx = ValidatorContext::new(&root_node.schema, &document);
|
let mut ctx = ValidatorContext::new(&root_node.schema, &document);
|
||||||
visit_all_rules(&mut ctx, &document);
|
visit_all_rules(&mut ctx, &document);
|
||||||
|
if root_node.introspection_disabled {
|
||||||
|
visit_rule(
|
||||||
|
&mut MultiVisitorNil.with(rules::disable_introspection::factory()),
|
||||||
|
&mut ctx,
|
||||||
|
&document,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let errors = ctx.into_errors();
|
let errors = ctx.into_errors();
|
||||||
if !errors.is_empty() {
|
if !errors.is_empty() {
|
||||||
|
@ -201,6 +212,13 @@ where
|
||||||
{
|
{
|
||||||
let mut ctx = ValidatorContext::new(&root_node.schema, &document);
|
let mut ctx = ValidatorContext::new(&root_node.schema, &document);
|
||||||
visit_all_rules(&mut ctx, &document);
|
visit_all_rules(&mut ctx, &document);
|
||||||
|
if root_node.introspection_disabled {
|
||||||
|
visit_rule(
|
||||||
|
&mut MultiVisitorNil.with(rules::disable_introspection::factory()),
|
||||||
|
&mut ctx,
|
||||||
|
&document,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let errors = ctx.into_errors();
|
let errors = ctx.into_errors();
|
||||||
if !errors.is_empty() {
|
if !errors.is_empty() {
|
||||||
|
@ -246,6 +264,13 @@ where
|
||||||
{
|
{
|
||||||
let mut ctx = ValidatorContext::new(&root_node.schema, &document);
|
let mut ctx = ValidatorContext::new(&root_node.schema, &document);
|
||||||
visit_all_rules(&mut ctx, &document);
|
visit_all_rules(&mut ctx, &document);
|
||||||
|
if root_node.introspection_disabled {
|
||||||
|
visit_rule(
|
||||||
|
&mut MultiVisitorNil.with(rules::disable_introspection::factory()),
|
||||||
|
&mut ctx,
|
||||||
|
&document,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let errors = ctx.into_errors();
|
let errors = ctx.into_errors();
|
||||||
if !errors.is_empty() {
|
if !errors.is_empty() {
|
||||||
|
@ -268,8 +293,8 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute the reference introspection query in the provided schema
|
/// Execute the reference introspection query in the provided schema
|
||||||
pub fn introspect<'a, S, QueryT, MutationT, SubscriptionT>(
|
pub fn introspect<S, QueryT, MutationT, SubscriptionT>(
|
||||||
root_node: &'a RootNode<QueryT, MutationT, SubscriptionT, S>,
|
root_node: &RootNode<QueryT, MutationT, SubscriptionT, S>,
|
||||||
context: &QueryT::Context,
|
context: &QueryT::Context,
|
||||||
format: IntrospectionFormat,
|
format: IntrospectionFormat,
|
||||||
) -> Result<(Value<S>, Vec<ExecutionError<S>>), GraphQLError>
|
) -> Result<(Value<S>, Vec<ExecutionError<S>>), GraphQLError>
|
||||||
|
|
|
@ -44,11 +44,11 @@ pub trait BaseType<S> {
|
||||||
const NAME: Type;
|
const NAME: Type;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, S, T: BaseType<S> + ?Sized> BaseType<S> for &'a T {
|
impl<S, T: BaseType<S> + ?Sized> BaseType<S> for &T {
|
||||||
const NAME: Type = T::NAME;
|
const NAME: Type = T::NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'ctx, S, T> BaseType<S> for (&'ctx T::Context, T)
|
impl<S, T> BaseType<S> for (&T::Context, T)
|
||||||
where
|
where
|
||||||
S: ScalarValue,
|
S: ScalarValue,
|
||||||
T: BaseType<S> + GraphQLValue<S>,
|
T: BaseType<S> + GraphQLValue<S>,
|
||||||
|
@ -105,11 +105,11 @@ pub trait BaseSubTypes<S> {
|
||||||
const NAMES: Types;
|
const NAMES: Types;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, S, T: BaseSubTypes<S> + ?Sized> BaseSubTypes<S> for &'a T {
|
impl<S, T: BaseSubTypes<S> + ?Sized> BaseSubTypes<S> for &T {
|
||||||
const NAMES: Types = T::NAMES;
|
const NAMES: Types = T::NAMES;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'ctx, S, T> BaseSubTypes<S> for (&'ctx T::Context, T)
|
impl<S, T> BaseSubTypes<S> for (&T::Context, T)
|
||||||
where
|
where
|
||||||
S: ScalarValue,
|
S: ScalarValue,
|
||||||
T: BaseSubTypes<S> + GraphQLValue<S>,
|
T: BaseSubTypes<S> + GraphQLValue<S>,
|
||||||
|
@ -205,7 +205,7 @@ pub trait WrappedType<S> {
|
||||||
const VALUE: WrappedValue;
|
const VALUE: WrappedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'ctx, S, T: WrappedType<S>> WrappedType<S> for (&'ctx T::Context, T)
|
impl<S, T: WrappedType<S>> WrappedType<S> for (&T::Context, T)
|
||||||
where
|
where
|
||||||
S: ScalarValue,
|
S: ScalarValue,
|
||||||
T: GraphQLValue<S>,
|
T: GraphQLValue<S>,
|
||||||
|
@ -237,7 +237,7 @@ impl<S, T: WrappedType<S>, const N: usize> WrappedType<S> for [T; N] {
|
||||||
const VALUE: u128 = T::VALUE * 10 + 3;
|
const VALUE: u128 = T::VALUE * 10 + 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, S, T: WrappedType<S> + ?Sized> WrappedType<S> for &'a T {
|
impl<S, T: WrappedType<S> + ?Sized> WrappedType<S> for &T {
|
||||||
const VALUE: u128 = T::VALUE;
|
const VALUE: u128 = T::VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -516,7 +516,7 @@ macro_rules! assert_implemented_for {
|
||||||
<$implementor as $crate::macros::reflect::BaseType<$scalar>>::NAME,
|
<$implementor as $crate::macros::reflect::BaseType<$scalar>>::NAME,
|
||||||
"`: missing implementer reference in interface's `for` attribute.",
|
"`: missing implementer reference in interface's `for` attribute.",
|
||||||
);
|
);
|
||||||
::std::panic!("{}", MSG);
|
::core::panic!("{}", MSG);
|
||||||
}
|
}
|
||||||
})*
|
})*
|
||||||
};
|
};
|
||||||
|
@ -544,7 +544,7 @@ macro_rules! assert_interfaces_impls {
|
||||||
<$implementers as $crate::macros::reflect::BaseType<$scalar>>::NAME,
|
<$implementers as $crate::macros::reflect::BaseType<$scalar>>::NAME,
|
||||||
"`: missing interface reference in implementer's `impl` attribute.",
|
"`: missing interface reference in implementer's `impl` attribute.",
|
||||||
);
|
);
|
||||||
::std::panic!("{}", MSG);
|
::core::panic!("{}", MSG);
|
||||||
}
|
}
|
||||||
})*
|
})*
|
||||||
};
|
};
|
||||||
|
@ -576,7 +576,7 @@ macro_rules! assert_transitive_impls {
|
||||||
<$implementor as $crate::macros::reflect::BaseType<$scalar>>::NAME,
|
<$implementor as $crate::macros::reflect::BaseType<$scalar>>::NAME,
|
||||||
"`."
|
"`."
|
||||||
);
|
);
|
||||||
::std::panic!("{}", MSG);
|
::core::panic!("{}", MSG);
|
||||||
}
|
}
|
||||||
})*
|
})*
|
||||||
};
|
};
|
||||||
|
@ -674,7 +674,7 @@ macro_rules! assert_subtype {
|
||||||
$crate::format_type!(BASE_RETURN_TY, BASE_RETURN_WRAPPED_VAL),
|
$crate::format_type!(BASE_RETURN_TY, BASE_RETURN_WRAPPED_VAL),
|
||||||
"`.",
|
"`.",
|
||||||
);
|
);
|
||||||
::std::panic!("{}", MSG);
|
::core::panic!("{}", MSG);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -693,9 +693,11 @@ macro_rules! assert_field_args {
|
||||||
$field_name: expr $(,)?
|
$field_name: expr $(,)?
|
||||||
) => {
|
) => {
|
||||||
const _: () = {
|
const _: () = {
|
||||||
const BASE_NAME: &str = <$base_ty as $crate::macros::reflect::BaseType<$scalar>>::NAME;
|
const BASE_NAME: &::core::primitive::str =
|
||||||
const IMPL_NAME: &str = <$impl_ty as $crate::macros::reflect::BaseType<$scalar>>::NAME;
|
<$base_ty as $crate::macros::reflect::BaseType<$scalar>>::NAME;
|
||||||
const ERR_PREFIX: &str = $crate::const_concat!(
|
const IMPL_NAME: &::core::primitive::str =
|
||||||
|
<$impl_ty as $crate::macros::reflect::BaseType<$scalar>>::NAME;
|
||||||
|
const ERR_PREFIX: &::core::primitive::str = $crate::const_concat!(
|
||||||
"Failed to implement interface `",
|
"Failed to implement interface `",
|
||||||
BASE_NAME,
|
BASE_NAME,
|
||||||
"` on `",
|
"` on `",
|
||||||
|
@ -703,7 +705,7 @@ macro_rules! assert_field_args {
|
||||||
"`: ",
|
"`: ",
|
||||||
);
|
);
|
||||||
|
|
||||||
const FIELD_NAME: &str = $field_name;
|
const FIELD_NAME: &::core::primitive::str = $field_name;
|
||||||
|
|
||||||
const BASE_ARGS: ::juniper::macros::reflect::Arguments =
|
const BASE_ARGS: ::juniper::macros::reflect::Arguments =
|
||||||
<$base_ty as $crate::macros::reflect::FieldMeta<
|
<$base_ty as $crate::macros::reflect::FieldMeta<
|
||||||
|
@ -728,20 +730,20 @@ macro_rules! assert_field_args {
|
||||||
TypeMismatch,
|
TypeMismatch,
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn unwrap_error(v: ::std::result::Result<(), Error>) -> Error {
|
const fn unwrap_error(v: ::core::result::Result<(), Error>) -> Error {
|
||||||
match v {
|
match v {
|
||||||
// Unfortunately we can't use `unreachable!()` here, as this
|
// Unfortunately we can't use `unreachable!()` here, as this
|
||||||
// branch will be executed either way.
|
// branch will be executed either way.
|
||||||
Ok(()) => Error {
|
::core::result::Result::Ok(()) => Error {
|
||||||
cause: Cause::RequiredField,
|
cause: Cause::RequiredField,
|
||||||
base: ("unreachable", "unreachable", 1),
|
base: ("unreachable", "unreachable", 1),
|
||||||
implementation: ("unreachable", "unreachable", 1),
|
implementation: ("unreachable", "unreachable", 1),
|
||||||
},
|
},
|
||||||
Err(err) => err,
|
::core::result::Result::Err(err) => err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn check() -> Result<(), Error> {
|
const fn check() -> ::core::result::Result<(), Error> {
|
||||||
let mut base_i = 0;
|
let mut base_i = 0;
|
||||||
while base_i < BASE_ARGS.len() {
|
while base_i < BASE_ARGS.len() {
|
||||||
let (base_name, base_type, base_wrap_val) = BASE_ARGS[base_i];
|
let (base_name, base_type, base_wrap_val) = BASE_ARGS[base_i];
|
||||||
|
@ -800,7 +802,7 @@ macro_rules! assert_field_args {
|
||||||
base_i += 1;
|
base_i += 1;
|
||||||
}
|
}
|
||||||
if !was_found {
|
if !was_found {
|
||||||
return Err(Error {
|
return ::core::result::Result::Err(Error {
|
||||||
cause: Cause::AdditionalNonNullableField,
|
cause: Cause::AdditionalNonNullableField,
|
||||||
base: (impl_name, impl_type, impl_wrapped_val),
|
base: (impl_name, impl_type, impl_wrapped_val),
|
||||||
implementation: (impl_name, impl_type, impl_wrapped_val),
|
implementation: (impl_name, impl_type, impl_wrapped_val),
|
||||||
|
@ -808,10 +810,10 @@ macro_rules! assert_field_args {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
::core::result::Result::Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
const RES: ::std::result::Result<(), Error> = check();
|
const RES: ::core::result::Result<(), Error> = check();
|
||||||
if RES.is_err() {
|
if RES.is_err() {
|
||||||
const ERROR: Error = unwrap_error(RES);
|
const ERROR: Error = unwrap_error(RES);
|
||||||
|
|
||||||
|
@ -822,7 +824,7 @@ macro_rules! assert_field_args {
|
||||||
const IMPL_TYPE_FORMATTED: &str =
|
const IMPL_TYPE_FORMATTED: &str =
|
||||||
$crate::format_type!(ERROR.implementation.1, ERROR.implementation.2);
|
$crate::format_type!(ERROR.implementation.1, ERROR.implementation.2);
|
||||||
|
|
||||||
const MSG: &str = match ERROR.cause {
|
const MSG: &::core::primitive::str = match ERROR.cause {
|
||||||
Cause::TypeMismatch => {
|
Cause::TypeMismatch => {
|
||||||
$crate::const_concat!(
|
$crate::const_concat!(
|
||||||
"Argument `",
|
"Argument `",
|
||||||
|
@ -855,7 +857,7 @@ macro_rules! assert_field_args {
|
||||||
};
|
};
|
||||||
const ERROR_MSG: &str =
|
const ERROR_MSG: &str =
|
||||||
$crate::const_concat!(ERR_PREFIX, "Field `", FIELD_NAME, "`: ", MSG);
|
$crate::const_concat!(ERR_PREFIX, "Field `", FIELD_NAME, "`: ", MSG);
|
||||||
::std::panic!("{}", ERROR_MSG);
|
::core::panic!("{}", ERROR_MSG);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -865,9 +867,9 @@ macro_rules! assert_field_args {
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! const_concat {
|
macro_rules! const_concat {
|
||||||
($($s:expr),* $(,)?) => {{
|
($($s:expr),* $(,)?) => {{
|
||||||
const LEN: usize = 0 $(+ $s.as_bytes().len())*;
|
const LEN: ::core::primitive::usize = 0 $(+ $s.as_bytes().len())*;
|
||||||
const CNT: usize = [$($s),*].len();
|
const CNT: ::core::primitive::usize = [$($s),*].len();
|
||||||
const fn concat(input: [&str; CNT]) -> [u8; LEN] {
|
const fn concat(input: [&::core::primitive::str; CNT]) -> [::core::primitive::u8; LEN] {
|
||||||
let mut bytes = [0; LEN];
|
let mut bytes = [0; LEN];
|
||||||
let (mut i, mut byte) = (0, 0);
|
let (mut i, mut byte) = (0, 0);
|
||||||
while i < CNT {
|
while i < CNT {
|
||||||
|
@ -881,12 +883,12 @@ macro_rules! const_concat {
|
||||||
}
|
}
|
||||||
bytes
|
bytes
|
||||||
}
|
}
|
||||||
const CON: [u8; LEN] = concat([$($s),*]);
|
const CON: [::core::primitive::u8; LEN] = concat([$($s),*]);
|
||||||
|
|
||||||
// TODO: Use `.unwrap()` once it becomes `const`.
|
// TODO: Use `.unwrap()` once it becomes `const`.
|
||||||
match ::std::str::from_utf8(&CON) {
|
match ::core::str::from_utf8(&CON) {
|
||||||
::std::result::Result::Ok(s) => s,
|
::core::result::Result::Ok(s) => s,
|
||||||
_ => unreachable!(),
|
_ => ::core::unreachable!(),
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
@ -911,7 +913,7 @@ macro_rules! checked_hash {
|
||||||
<$impl_ty as $crate::macros::reflect::BaseType<$scalar>>::NAME,
|
<$impl_ty as $crate::macros::reflect::BaseType<$scalar>>::NAME,
|
||||||
"`."
|
"`."
|
||||||
);
|
);
|
||||||
::std::panic!("{}", MSG)
|
::core::panic!("{}", MSG)
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
@ -936,13 +938,13 @@ macro_rules! format_type {
|
||||||
) = ($ty, $wrapped_value);
|
) = ($ty, $wrapped_value);
|
||||||
const RES_LEN: usize = $crate::macros::reflect::type_len_with_wrapped_val(TYPE.0, TYPE.1);
|
const RES_LEN: usize = $crate::macros::reflect::type_len_with_wrapped_val(TYPE.0, TYPE.1);
|
||||||
|
|
||||||
const OPENING_BRACKET: &str = "[";
|
const OPENING_BRACKET: &::core::primitive::str = "[";
|
||||||
const CLOSING_BRACKET: &str = "]";
|
const CLOSING_BRACKET: &::core::primitive::str = "]";
|
||||||
const BANG: &str = "!";
|
const BANG: &::core::primitive::str = "!";
|
||||||
|
|
||||||
const fn format_type_arr() -> [u8; RES_LEN] {
|
const fn format_type_arr() -> [::core::primitive::u8; RES_LEN] {
|
||||||
let (ty, wrap_val) = TYPE;
|
let (ty, wrap_val) = TYPE;
|
||||||
let mut type_arr: [u8; RES_LEN] = [0; RES_LEN];
|
let mut type_arr: [::core::primitive::u8; RES_LEN] = [0; RES_LEN];
|
||||||
|
|
||||||
let mut current_start = 0;
|
let mut current_start = 0;
|
||||||
let mut current_end = RES_LEN - 1;
|
let mut current_end = RES_LEN - 1;
|
||||||
|
@ -1003,13 +1005,14 @@ macro_rules! format_type {
|
||||||
type_arr
|
type_arr
|
||||||
}
|
}
|
||||||
|
|
||||||
const TYPE_ARR: [u8; RES_LEN] = format_type_arr();
|
const TYPE_ARR: [::core::primitive::u8; RES_LEN] = format_type_arr();
|
||||||
|
|
||||||
// TODO: Use `.unwrap()` once it becomes `const`.
|
// TODO: Use `.unwrap()` once it becomes `const`.
|
||||||
const TYPE_FORMATTED: &str = match ::std::str::from_utf8(TYPE_ARR.as_slice()) {
|
const TYPE_FORMATTED: &::core::primitive::str =
|
||||||
::std::result::Result::Ok(s) => s,
|
match ::core::str::from_utf8(TYPE_ARR.as_slice()) {
|
||||||
_ => unreachable!(),
|
::core::result::Result::Ok(s) => s,
|
||||||
};
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
TYPE_FORMATTED
|
TYPE_FORMATTED
|
||||||
}};
|
}};
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue