├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-MIT ├── README.md ├── graph.svg ├── rustfmt.toml ├── src ├── bin.rs ├── config.rs ├── extend_types.rs ├── lib.rs ├── state.rs └── utils.rs └── tests ├── fixtures ├── Directives │ ├── deprecated.graphql │ └── test.graphql ├── Types │ ├── Enums │ │ ├── Episode.gql │ │ ├── EpisodeExtension.graphql │ │ ├── LengthUnit.graphql │ │ └── Letter.gql │ ├── Inputs │ │ ├── a.gql │ │ └── extension.gql │ ├── Interfaces │ │ ├── Character.graphql │ │ └── CharacterExtension.gql │ ├── Scalars │ │ ├── DateTime.graphql │ │ └── DateTimeExtension.gql │ ├── Types │ │ ├── a.gql │ │ ├── b.graphql │ │ ├── c.gql │ │ ├── extension.graphql │ │ └── orphan.gql │ └── Unions │ │ ├── SearchResultExtension.graphql │ │ └── SearchResults.graphql ├── c.txt └── schema.graphql └── integration.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | clippy: 11 | if: "!contains(toJSON(github.event.commits.*.message), '[skip ci]')" 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: hecrj/setup-rust-action@v1 15 | with: 16 | rust-version: stable 17 | components: clippy 18 | - uses: actions/checkout@v2 19 | - name: Clippy 20 | run: cargo clippy --all-features -- -D warnings 21 | 22 | test: 23 | if: "!contains(toJSON(github.event.commits.*.message), '[skip ci]')" 24 | runs-on: ${{ matrix.os }} 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | os: [ubuntu-latest, windows-latest, macOS-latest] 29 | rust: [stable, beta, nightly] 30 | steps: 31 | - uses: hecrj/setup-rust-action@v1 32 | with: 33 | rust-version: ${{ matrix.rust }} 34 | - uses: actions/checkout@v2 35 | - name: Run cargo tests 36 | run: cargo test --all-features --verbose 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target 3 | **/*.rs.bk -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anstream" 7 | version = "0.3.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "is-terminal", 17 | "utf8parse", 18 | ] 19 | 20 | [[package]] 21 | name = "anstyle" 22 | version = "1.0.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" 25 | 26 | [[package]] 27 | name = "anstyle-parse" 28 | version = "0.2.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" 31 | dependencies = [ 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle-query" 37 | version = "1.0.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 40 | dependencies = [ 41 | "windows-sys 0.48.0", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-wincon" 46 | version = "1.0.1" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" 49 | dependencies = [ 50 | "anstyle", 51 | "windows-sys 0.48.0", 52 | ] 53 | 54 | [[package]] 55 | name = "anyhow" 56 | version = "1.0.71" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" 59 | 60 | [[package]] 61 | name = "ascii" 62 | version = "0.9.3" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" 65 | 66 | [[package]] 67 | name = "async-attributes" 68 | version = "1.1.1" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "efd3d156917d94862e779f356c5acae312b08fd3121e792c857d7928c8088423" 71 | dependencies = [ 72 | "quote", 73 | "syn 1.0.76", 74 | ] 75 | 76 | [[package]] 77 | name = "async-channel" 78 | version = "1.5.1" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "59740d83946db6a5af71ae25ddf9562c2b176b2ca42cf99a455f09f4a220d6b9" 81 | dependencies = [ 82 | "concurrent-queue", 83 | "event-listener", 84 | "futures-core", 85 | ] 86 | 87 | [[package]] 88 | name = "async-executor" 89 | version = "1.4.0" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "eb877970c7b440ead138f6321a3b5395d6061183af779340b65e20c0fede9146" 92 | dependencies = [ 93 | "async-task", 94 | "concurrent-queue", 95 | "fastrand", 96 | "futures-lite", 97 | "once_cell", 98 | "vec-arena", 99 | ] 100 | 101 | [[package]] 102 | name = "async-global-executor" 103 | version = "2.0.2" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "9586ec52317f36de58453159d48351bc244bc24ced3effc1fce22f3d48664af6" 106 | dependencies = [ 107 | "async-channel", 108 | "async-executor", 109 | "async-io", 110 | "async-mutex", 111 | "blocking", 112 | "futures-lite", 113 | "num_cpus", 114 | "once_cell", 115 | ] 116 | 117 | [[package]] 118 | name = "async-io" 119 | version = "1.3.1" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "9315f8f07556761c3e48fec2e6b276004acf426e6dc068b2c2251854d65ee0fd" 122 | dependencies = [ 123 | "concurrent-queue", 124 | "fastrand", 125 | "futures-lite", 126 | "libc", 127 | "log", 128 | "nb-connect", 129 | "once_cell", 130 | "parking", 131 | "polling", 132 | "vec-arena", 133 | "waker-fn", 134 | "winapi", 135 | ] 136 | 137 | [[package]] 138 | name = "async-lock" 139 | version = "2.3.0" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "1996609732bde4a9988bc42125f55f2af5f3c36370e27c778d5191a4a1b63bfb" 142 | dependencies = [ 143 | "event-listener", 144 | ] 145 | 146 | [[package]] 147 | name = "async-mutex" 148 | version = "1.4.0" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" 151 | dependencies = [ 152 | "event-listener", 153 | ] 154 | 155 | [[package]] 156 | name = "async-process" 157 | version = "1.0.1" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "4c8cea09c1fb10a317d1b5af8024eeba256d6554763e85ecd90ff8df31c7bbda" 160 | dependencies = [ 161 | "async-io", 162 | "blocking", 163 | "cfg-if 0.1.10", 164 | "event-listener", 165 | "futures-lite", 166 | "once_cell", 167 | "signal-hook", 168 | "winapi", 169 | ] 170 | 171 | [[package]] 172 | name = "async-std" 173 | version = "1.12.0" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" 176 | dependencies = [ 177 | "async-attributes", 178 | "async-channel", 179 | "async-global-executor", 180 | "async-io", 181 | "async-lock", 182 | "async-process", 183 | "crossbeam-utils", 184 | "futures-channel", 185 | "futures-core", 186 | "futures-io", 187 | "futures-lite", 188 | "gloo-timers", 189 | "kv-log-macro", 190 | "log", 191 | "memchr", 192 | "once_cell", 193 | "pin-project-lite 0.2.4", 194 | "pin-utils", 195 | "slab", 196 | "wasm-bindgen-futures", 197 | ] 198 | 199 | [[package]] 200 | name = "async-task" 201 | version = "4.0.2" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "8ab27c1aa62945039e44edaeee1dc23c74cc0c303dd5fe0fb462a184f1c3a518" 204 | 205 | [[package]] 206 | name = "atomic-waker" 207 | version = "1.0.0" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" 210 | 211 | [[package]] 212 | name = "autocfg" 213 | version = "1.0.0" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 216 | 217 | [[package]] 218 | name = "bitflags" 219 | version = "1.2.1" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 222 | 223 | [[package]] 224 | name = "blocking" 225 | version = "1.0.2" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "c5e170dbede1f740736619b776d7251cb1b9095c435c34d8ca9f57fcd2f335e9" 228 | dependencies = [ 229 | "async-channel", 230 | "async-task", 231 | "atomic-waker", 232 | "fastrand", 233 | "futures-lite", 234 | "once_cell", 235 | ] 236 | 237 | [[package]] 238 | name = "bumpalo" 239 | version = "3.4.0" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" 242 | 243 | [[package]] 244 | name = "byteorder" 245 | version = "1.3.4" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 248 | 249 | [[package]] 250 | name = "cache-padded" 251 | version = "1.1.1" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" 254 | 255 | [[package]] 256 | name = "cc" 257 | version = "1.0.58" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518" 260 | 261 | [[package]] 262 | name = "cfg-if" 263 | version = "0.1.10" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 266 | 267 | [[package]] 268 | name = "cfg-if" 269 | version = "1.0.0" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 272 | 273 | [[package]] 274 | name = "clap" 275 | version = "4.2.7" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938" 278 | dependencies = [ 279 | "clap_builder", 280 | "clap_derive", 281 | "once_cell", 282 | ] 283 | 284 | [[package]] 285 | name = "clap_builder" 286 | version = "4.2.7" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd" 289 | dependencies = [ 290 | "anstream", 291 | "anstyle", 292 | "bitflags", 293 | "clap_lex", 294 | "strsim", 295 | ] 296 | 297 | [[package]] 298 | name = "clap_derive" 299 | version = "4.2.0" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" 302 | dependencies = [ 303 | "heck", 304 | "proc-macro2", 305 | "quote", 306 | "syn 2.0.16", 307 | ] 308 | 309 | [[package]] 310 | name = "clap_lex" 311 | version = "0.4.1" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" 314 | 315 | [[package]] 316 | name = "colorchoice" 317 | version = "1.0.0" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 320 | 321 | [[package]] 322 | name = "combine" 323 | version = "3.8.1" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" 326 | dependencies = [ 327 | "ascii", 328 | "byteorder", 329 | "either", 330 | "memchr", 331 | "unreachable", 332 | ] 333 | 334 | [[package]] 335 | name = "concurrent-queue" 336 | version = "1.2.2" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" 339 | dependencies = [ 340 | "cache-padded", 341 | ] 342 | 343 | [[package]] 344 | name = "craftql" 345 | version = "0.2.20" 346 | dependencies = [ 347 | "anyhow", 348 | "async-std", 349 | "clap", 350 | "graphql-parser", 351 | "petgraph", 352 | ] 353 | 354 | [[package]] 355 | name = "crossbeam-utils" 356 | version = "0.8.1" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" 359 | dependencies = [ 360 | "autocfg", 361 | "cfg-if 1.0.0", 362 | "lazy_static", 363 | ] 364 | 365 | [[package]] 366 | name = "either" 367 | version = "1.5.3" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "bb1f6b1ce1c140482ea30ddd3335fc0024ac7ee112895426e0a629a6c20adfe3" 370 | 371 | [[package]] 372 | name = "errno" 373 | version = "0.3.1" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" 376 | dependencies = [ 377 | "errno-dragonfly", 378 | "libc", 379 | "windows-sys 0.48.0", 380 | ] 381 | 382 | [[package]] 383 | name = "errno-dragonfly" 384 | version = "0.1.2" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 387 | dependencies = [ 388 | "cc", 389 | "libc", 390 | ] 391 | 392 | [[package]] 393 | name = "event-listener" 394 | version = "2.5.1" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" 397 | 398 | [[package]] 399 | name = "fastrand" 400 | version = "1.4.0" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "ca5faf057445ce5c9d4329e382b2ce7ca38550ef3b73a5348362d5f24e0c7fe3" 403 | dependencies = [ 404 | "instant", 405 | ] 406 | 407 | [[package]] 408 | name = "fixedbitset" 409 | version = "0.4.0" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "398ea4fabe40b9b0d885340a2a991a44c8a645624075ad966d21f88688e2b69e" 412 | 413 | [[package]] 414 | name = "futures-channel" 415 | version = "0.3.5" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" 418 | dependencies = [ 419 | "futures-core", 420 | ] 421 | 422 | [[package]] 423 | name = "futures-core" 424 | version = "0.3.5" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" 427 | 428 | [[package]] 429 | name = "futures-io" 430 | version = "0.3.5" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" 433 | 434 | [[package]] 435 | name = "futures-lite" 436 | version = "1.11.1" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "381a7ad57b1bad34693f63f6f377e1abded7a9c85c9d3eb6771e11c60aaadab9" 439 | dependencies = [ 440 | "fastrand", 441 | "futures-core", 442 | "futures-io", 443 | "memchr", 444 | "parking", 445 | "pin-project-lite 0.1.7", 446 | "waker-fn", 447 | ] 448 | 449 | [[package]] 450 | name = "gloo-timers" 451 | version = "0.2.1" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f" 454 | dependencies = [ 455 | "futures-channel", 456 | "futures-core", 457 | "js-sys", 458 | "wasm-bindgen", 459 | "web-sys", 460 | ] 461 | 462 | [[package]] 463 | name = "graphql-parser" 464 | version = "0.4.0" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "d2ebc8013b4426d5b81a4364c419a95ed0b404af2b82e2457de52d9348f0e474" 467 | dependencies = [ 468 | "combine", 469 | "thiserror", 470 | ] 471 | 472 | [[package]] 473 | name = "hashbrown" 474 | version = "0.11.2" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 477 | 478 | [[package]] 479 | name = "heck" 480 | version = "0.4.0" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 483 | 484 | [[package]] 485 | name = "hermit-abi" 486 | version = "0.1.15" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" 489 | dependencies = [ 490 | "libc", 491 | ] 492 | 493 | [[package]] 494 | name = "hermit-abi" 495 | version = "0.3.1" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 498 | 499 | [[package]] 500 | name = "indexmap" 501 | version = "1.7.0" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" 504 | dependencies = [ 505 | "autocfg", 506 | "hashbrown", 507 | ] 508 | 509 | [[package]] 510 | name = "instant" 511 | version = "0.1.7" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "63312a18f7ea8760cdd0a7c5aac1a619752a246b833545e3e36d1f81f7cd9e66" 514 | dependencies = [ 515 | "cfg-if 0.1.10", 516 | ] 517 | 518 | [[package]] 519 | name = "io-lifetimes" 520 | version = "1.0.3" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" 523 | dependencies = [ 524 | "libc", 525 | "windows-sys 0.42.0", 526 | ] 527 | 528 | [[package]] 529 | name = "is-terminal" 530 | version = "0.4.7" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" 533 | dependencies = [ 534 | "hermit-abi 0.3.1", 535 | "io-lifetimes", 536 | "rustix", 537 | "windows-sys 0.48.0", 538 | ] 539 | 540 | [[package]] 541 | name = "js-sys" 542 | version = "0.3.44" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "85a7e2c92a4804dd459b86c339278d0fe87cf93757fae222c3fa3ae75458bc73" 545 | dependencies = [ 546 | "wasm-bindgen", 547 | ] 548 | 549 | [[package]] 550 | name = "kv-log-macro" 551 | version = "1.0.7" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" 554 | dependencies = [ 555 | "log", 556 | ] 557 | 558 | [[package]] 559 | name = "lazy_static" 560 | version = "1.4.0" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 563 | 564 | [[package]] 565 | name = "libc" 566 | version = "0.2.144" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" 569 | 570 | [[package]] 571 | name = "linux-raw-sys" 572 | version = "0.3.7" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" 575 | 576 | [[package]] 577 | name = "log" 578 | version = "0.4.11" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" 581 | dependencies = [ 582 | "cfg-if 0.1.10", 583 | ] 584 | 585 | [[package]] 586 | name = "memchr" 587 | version = "2.4.1" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 590 | 591 | [[package]] 592 | name = "nb-connect" 593 | version = "1.0.2" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "8123a81538e457d44b933a02faf885d3fe8408806b23fa700e8f01c6c3a98998" 596 | dependencies = [ 597 | "libc", 598 | "winapi", 599 | ] 600 | 601 | [[package]] 602 | name = "num_cpus" 603 | version = "1.13.0" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 606 | dependencies = [ 607 | "hermit-abi 0.1.15", 608 | "libc", 609 | ] 610 | 611 | [[package]] 612 | name = "once_cell" 613 | version = "1.12.0" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" 616 | 617 | [[package]] 618 | name = "parking" 619 | version = "2.0.0" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" 622 | 623 | [[package]] 624 | name = "petgraph" 625 | version = "0.6.3" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" 628 | dependencies = [ 629 | "fixedbitset", 630 | "indexmap", 631 | ] 632 | 633 | [[package]] 634 | name = "pin-project-lite" 635 | version = "0.1.7" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715" 638 | 639 | [[package]] 640 | name = "pin-project-lite" 641 | version = "0.2.4" 642 | source = "registry+https://github.com/rust-lang/crates.io-index" 643 | checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827" 644 | 645 | [[package]] 646 | name = "pin-utils" 647 | version = "0.1.0" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 650 | 651 | [[package]] 652 | name = "polling" 653 | version = "2.0.2" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "a2a7bc6b2a29e632e45451c941832803a18cce6781db04de8a04696cdca8bde4" 656 | dependencies = [ 657 | "cfg-if 0.1.10", 658 | "libc", 659 | "log", 660 | "wepoll-sys", 661 | "winapi", 662 | ] 663 | 664 | [[package]] 665 | name = "proc-macro2" 666 | version = "1.0.58" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" 669 | dependencies = [ 670 | "unicode-ident", 671 | ] 672 | 673 | [[package]] 674 | name = "quote" 675 | version = "1.0.27" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" 678 | dependencies = [ 679 | "proc-macro2", 680 | ] 681 | 682 | [[package]] 683 | name = "rustix" 684 | version = "0.37.7" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d" 687 | dependencies = [ 688 | "bitflags", 689 | "errno", 690 | "io-lifetimes", 691 | "libc", 692 | "linux-raw-sys", 693 | "windows-sys 0.45.0", 694 | ] 695 | 696 | [[package]] 697 | name = "signal-hook" 698 | version = "0.1.17" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729" 701 | dependencies = [ 702 | "libc", 703 | "signal-hook-registry", 704 | ] 705 | 706 | [[package]] 707 | name = "signal-hook-registry" 708 | version = "1.3.0" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" 711 | dependencies = [ 712 | "libc", 713 | ] 714 | 715 | [[package]] 716 | name = "slab" 717 | version = "0.4.2" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" 720 | 721 | [[package]] 722 | name = "strsim" 723 | version = "0.10.0" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 726 | 727 | [[package]] 728 | name = "syn" 729 | version = "1.0.76" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84" 732 | dependencies = [ 733 | "proc-macro2", 734 | "quote", 735 | "unicode-xid", 736 | ] 737 | 738 | [[package]] 739 | name = "syn" 740 | version = "2.0.16" 741 | source = "registry+https://github.com/rust-lang/crates.io-index" 742 | checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" 743 | dependencies = [ 744 | "proc-macro2", 745 | "quote", 746 | "unicode-ident", 747 | ] 748 | 749 | [[package]] 750 | name = "thiserror" 751 | version = "1.0.20" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" 754 | dependencies = [ 755 | "thiserror-impl", 756 | ] 757 | 758 | [[package]] 759 | name = "thiserror-impl" 760 | version = "1.0.20" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" 763 | dependencies = [ 764 | "proc-macro2", 765 | "quote", 766 | "syn 1.0.76", 767 | ] 768 | 769 | [[package]] 770 | name = "unicode-ident" 771 | version = "1.0.6" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 774 | 775 | [[package]] 776 | name = "unicode-xid" 777 | version = "0.2.1" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 780 | 781 | [[package]] 782 | name = "unreachable" 783 | version = "1.0.0" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" 786 | dependencies = [ 787 | "void", 788 | ] 789 | 790 | [[package]] 791 | name = "utf8parse" 792 | version = "0.2.1" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 795 | 796 | [[package]] 797 | name = "vec-arena" 798 | version = "1.0.0" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "eafc1b9b2dfc6f5529177b62cf806484db55b32dc7c9658a118e11bbeb33061d" 801 | 802 | [[package]] 803 | name = "void" 804 | version = "1.0.2" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 807 | 808 | [[package]] 809 | name = "waker-fn" 810 | version = "1.1.0" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" 813 | 814 | [[package]] 815 | name = "wasm-bindgen" 816 | version = "0.2.67" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "f0563a9a4b071746dd5aedbc3a28c6fe9be4586fb3fbadb67c400d4f53c6b16c" 819 | dependencies = [ 820 | "cfg-if 0.1.10", 821 | "wasm-bindgen-macro", 822 | ] 823 | 824 | [[package]] 825 | name = "wasm-bindgen-backend" 826 | version = "0.2.67" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "bc71e4c5efa60fb9e74160e89b93353bc24059999c0ae0fb03affc39770310b0" 829 | dependencies = [ 830 | "bumpalo", 831 | "lazy_static", 832 | "log", 833 | "proc-macro2", 834 | "quote", 835 | "syn 1.0.76", 836 | "wasm-bindgen-shared", 837 | ] 838 | 839 | [[package]] 840 | name = "wasm-bindgen-futures" 841 | version = "0.4.17" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "95f8d235a77f880bcef268d379810ea6c0af2eacfa90b1ad5af731776e0c4699" 844 | dependencies = [ 845 | "cfg-if 0.1.10", 846 | "js-sys", 847 | "wasm-bindgen", 848 | "web-sys", 849 | ] 850 | 851 | [[package]] 852 | name = "wasm-bindgen-macro" 853 | version = "0.2.67" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "97c57cefa5fa80e2ba15641578b44d36e7a64279bc5ed43c6dbaf329457a2ed2" 856 | dependencies = [ 857 | "quote", 858 | "wasm-bindgen-macro-support", 859 | ] 860 | 861 | [[package]] 862 | name = "wasm-bindgen-macro-support" 863 | version = "0.2.67" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "841a6d1c35c6f596ccea1f82504a192a60378f64b3bb0261904ad8f2f5657556" 866 | dependencies = [ 867 | "proc-macro2", 868 | "quote", 869 | "syn 1.0.76", 870 | "wasm-bindgen-backend", 871 | "wasm-bindgen-shared", 872 | ] 873 | 874 | [[package]] 875 | name = "wasm-bindgen-shared" 876 | version = "0.2.67" 877 | source = "registry+https://github.com/rust-lang/crates.io-index" 878 | checksum = "93b162580e34310e5931c4b792560108b10fd14d64915d7fff8ff00180e70092" 879 | 880 | [[package]] 881 | name = "web-sys" 882 | version = "0.3.44" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "dda38f4e5ca63eda02c059d243aa25b5f35ab98451e518c51612cd0f1bd19a47" 885 | dependencies = [ 886 | "js-sys", 887 | "wasm-bindgen", 888 | ] 889 | 890 | [[package]] 891 | name = "wepoll-sys" 892 | version = "3.0.1" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "0fcb14dea929042224824779fbc82d9fab8d2e6d3cbc0ac404de8edf489e77ff" 895 | dependencies = [ 896 | "cc", 897 | ] 898 | 899 | [[package]] 900 | name = "winapi" 901 | version = "0.3.9" 902 | source = "registry+https://github.com/rust-lang/crates.io-index" 903 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 904 | dependencies = [ 905 | "winapi-i686-pc-windows-gnu", 906 | "winapi-x86_64-pc-windows-gnu", 907 | ] 908 | 909 | [[package]] 910 | name = "winapi-i686-pc-windows-gnu" 911 | version = "0.4.0" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 914 | 915 | [[package]] 916 | name = "winapi-x86_64-pc-windows-gnu" 917 | version = "0.4.0" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 920 | 921 | [[package]] 922 | name = "windows-sys" 923 | version = "0.42.0" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 926 | dependencies = [ 927 | "windows_aarch64_gnullvm 0.42.2", 928 | "windows_aarch64_msvc 0.42.2", 929 | "windows_i686_gnu 0.42.2", 930 | "windows_i686_msvc 0.42.2", 931 | "windows_x86_64_gnu 0.42.2", 932 | "windows_x86_64_gnullvm 0.42.2", 933 | "windows_x86_64_msvc 0.42.2", 934 | ] 935 | 936 | [[package]] 937 | name = "windows-sys" 938 | version = "0.45.0" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 941 | dependencies = [ 942 | "windows-targets 0.42.2", 943 | ] 944 | 945 | [[package]] 946 | name = "windows-sys" 947 | version = "0.48.0" 948 | source = "registry+https://github.com/rust-lang/crates.io-index" 949 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 950 | dependencies = [ 951 | "windows-targets 0.48.0", 952 | ] 953 | 954 | [[package]] 955 | name = "windows-targets" 956 | version = "0.42.2" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 959 | dependencies = [ 960 | "windows_aarch64_gnullvm 0.42.2", 961 | "windows_aarch64_msvc 0.42.2", 962 | "windows_i686_gnu 0.42.2", 963 | "windows_i686_msvc 0.42.2", 964 | "windows_x86_64_gnu 0.42.2", 965 | "windows_x86_64_gnullvm 0.42.2", 966 | "windows_x86_64_msvc 0.42.2", 967 | ] 968 | 969 | [[package]] 970 | name = "windows-targets" 971 | version = "0.48.0" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 974 | dependencies = [ 975 | "windows_aarch64_gnullvm 0.48.0", 976 | "windows_aarch64_msvc 0.48.0", 977 | "windows_i686_gnu 0.48.0", 978 | "windows_i686_msvc 0.48.0", 979 | "windows_x86_64_gnu 0.48.0", 980 | "windows_x86_64_gnullvm 0.48.0", 981 | "windows_x86_64_msvc 0.48.0", 982 | ] 983 | 984 | [[package]] 985 | name = "windows_aarch64_gnullvm" 986 | version = "0.42.2" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 989 | 990 | [[package]] 991 | name = "windows_aarch64_gnullvm" 992 | version = "0.48.0" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 995 | 996 | [[package]] 997 | name = "windows_aarch64_msvc" 998 | version = "0.42.2" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 1001 | 1002 | [[package]] 1003 | name = "windows_aarch64_msvc" 1004 | version = "0.48.0" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 1007 | 1008 | [[package]] 1009 | name = "windows_i686_gnu" 1010 | version = "0.42.2" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 1013 | 1014 | [[package]] 1015 | name = "windows_i686_gnu" 1016 | version = "0.48.0" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 1019 | 1020 | [[package]] 1021 | name = "windows_i686_msvc" 1022 | version = "0.42.2" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 1025 | 1026 | [[package]] 1027 | name = "windows_i686_msvc" 1028 | version = "0.48.0" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 1031 | 1032 | [[package]] 1033 | name = "windows_x86_64_gnu" 1034 | version = "0.42.2" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 1037 | 1038 | [[package]] 1039 | name = "windows_x86_64_gnu" 1040 | version = "0.48.0" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 1043 | 1044 | [[package]] 1045 | name = "windows_x86_64_gnullvm" 1046 | version = "0.42.2" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 1049 | 1050 | [[package]] 1051 | name = "windows_x86_64_gnullvm" 1052 | version = "0.48.0" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 1055 | 1056 | [[package]] 1057 | name = "windows_x86_64_msvc" 1058 | version = "0.42.2" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1061 | 1062 | [[package]] 1063 | name = "windows_x86_64_msvc" 1064 | version = "0.48.0" 1065 | source = "registry+https://github.com/rust-lang/crates.io-index" 1066 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 1067 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Davy Duperron "] 3 | categories = ["command-line-utilities", "development-tools"] 4 | description = "A CLI tool to visualize GraphQL schemas and to output a graph data structure as a graphviz .dot format" 5 | edition = "2021" 6 | exclude = [".github", "graph.svg", "rustfmt.toml"] 7 | keywords = ["cli", "graph", "graphql", "graphviz", "terminal"] 8 | license = "MIT" 9 | name = "craftql" 10 | readme = "README.md" 11 | repository = "https://github.com/yamafaktory/craftql" 12 | rust-version = "1.56" 13 | version = "0.2.20" 14 | 15 | [lib] 16 | name = "craftql" 17 | path = "src/lib.rs" 18 | 19 | [[bin]] 20 | name = "craftql" 21 | path = "src/bin.rs" 22 | 23 | [dependencies] 24 | anyhow = "1.0.71" 25 | clap = { version = "4.2.7", features = ["derive"] } 26 | graphql-parser = "0.4.0" 27 | petgraph = "0.6.3" 28 | 29 | [dependencies.async-std] 30 | version = "1.12.0" 31 | features = ["attributes", "unstable"] 32 | 33 | [profile.release] 34 | codegen-units = 1 35 | lto = true 36 | opt-level = 'z' 37 | panic = 'abort' 38 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) Davy Duperron 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CraftQL 2 | 3 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/yamafaktory/craftql/ci.yml?branch=main&logo=github&style=flat-square) ![Crates.io](https://img.shields.io/crates/v/craftql?style=flat-square) 4 | 5 | > A CLI tool to visualize GraphQL schemas and to output a graph data structure as a graphviz .dot format 6 | 7 | ## Installation 8 | 9 | ```sh 10 | cargo install craftql 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```sh 16 | USAGE: 17 | craftql [FLAGS] [OPTIONS] 18 | 19 | ARGS: 20 | 21 | Path to get files from 22 | 23 | FLAGS: 24 | -h, --help 25 | Prints help information 26 | 27 | -m, --missing-definitions 28 | Finds and displays missing definition(s) 29 | 30 | -O, --orphans 31 | Finds and displays orphan(s) node(s) 32 | 33 | -V, --version 34 | Prints version information 35 | 36 | 37 | OPTIONS: 38 | -f, --filter ... 39 | Filter nodes by GraphQL type(s) 40 | 41 | - directive 42 | - enum 43 | - enum_extension 44 | - input_object 45 | - input_object_extension 46 | - interface 47 | - interface_extension 48 | - object 49 | - object_extension 50 | - scalar 51 | - scalar_extension 52 | - schema 53 | - union 54 | - union_extension 55 | -i, --incoming-dependencies 56 | Finds and displays incoming dependencies of a node 57 | 58 | -n, --node 59 | Finds and displays one node 60 | 61 | -N, --nodes ... 62 | Finds and displays multiple nodes 63 | 64 | -o, --outgoing-dependencies 65 | Finds and displays outgoing dependencies of a node 66 | ``` 67 | 68 | ### Output a graphviz .dot format 69 | 70 | ```sh 71 | craftql tests/fixtures 72 | 73 | digraph { 74 | 0 [ label = "DateTime (Scalar)" ] 75 | 1 [ label = "Character (Interface extension)\l\l[Boolean, Character]" ] 76 | 2 [ label = "Human (Object)\l\l[Character, Episode, Float, FriendsConnection, ID, Int, LengthUnit, Starship, String]" ] 77 | 3 [ label = "Droid (Object)\l\l[Character, Episode, FriendsConnection, ID, Int, String]" ] 78 | 4 [ label = "FriendsConnection (Object)\l\l[Character, FriendsEdge, Int, PageInfo]" ] 79 | 5 [ label = "FriendsEdge (Object)\l\l[Character, ID]" ] 80 | 6 [ label = "PageInfo (Object)\l\l[Boolean, ID, test]" ] 81 | 7 [ label = "Review (Object)\l\l[DateTime, Episode, Int, String, test]" ] 82 | 8 [ label = "Orphan (Object)\l\l[ID]" ] 83 | 9 [ label = "ColorInput (InputObject extension)\l\l[ColorInput, Int]" ] 84 | 10 [ label = "Episode (Enum extension)\l\l[Episode]" ] 85 | 11 [ label = "deprecated (Directive)\l\l[String]" ] 86 | 12 [ label = "DateTime (Scalar extension)\l\l[DateTime, test]" ] 87 | 13 [ label = "SearchResult (Union extension)\l\l[Ewok, SearchResult]" ] 88 | 14 [ label = "test (Directive)\l\l[Letter]" ] 89 | 15 [ label = "Episode (Enum)\l\l[deprecated, test]" ] 90 | 16 [ label = "LengthUnit (Enum)" ] 91 | 17 [ label = "Starship (Object extension)\l\l[Boolean, Starship]" ] 92 | 18 [ label = "Query (Object)\l\l[Character, Droid, Episode, Human, ID, Review, SearchResult, Starship, String]" ] 93 | 19 [ label = "Mutation (Object)\l\l[Episode, Review, ReviewInput]" ] 94 | 20 [ label = "Subscription (Object)\l\l[Episode, Review]" ] 95 | 21 [ label = "schema (Schema)\l\l[Mutation, Query, Subscription]" ] 96 | 22 [ label = "ReviewInput (InputObject)\l\l[ColorInput, Int, ReviewInput, String]" ] 97 | 23 [ label = "ColorInput (InputObject)\l\l[ColorInput, deprecated, Int, test]" ] 98 | 24 [ label = "Letter (Enum)" ] 99 | 25 [ label = "Starship (Object)\l\l[deprecated, Float, ID, LengthUnit, String]" ] 100 | 26 [ label = "Character (Interface)\l\l[Bool, Character, deprecated, Episode, FriendsConnection, ID, Int, String, test]" ] 101 | 27 [ label = "SearchResult (Union)\l\l[Droid, Human, Starship, test]" ] 102 | 13 -> 27 [ ] 103 | 14 -> 6 [ ] 104 | 26 -> 4 [ ] 105 | 5 -> 4 [ ] 106 | 6 -> 4 [ ] 107 | 11 -> 25 [ ] 108 | 16 -> 25 [ ] 109 | 26 -> 26 [ ] 110 | 11 -> 26 [ ] 111 | 15 -> 26 [ ] 112 | 4 -> 26 [ ] 113 | 14 -> 26 [ ] 114 | 26 -> 3 [ ] 115 | 15 -> 3 [ ] 116 | 4 -> 3 [ ] 117 | 17 -> 25 [ ] 118 | 15 -> 20 [ ] 119 | 7 -> 20 [ ] 120 | 19 -> 21 [ ] 121 | 18 -> 21 [ ] 122 | 20 -> 21 [ ] 123 | 9 -> 23 [ ] 124 | 26 -> 18 [ ] 125 | 3 -> 18 [ ] 126 | 15 -> 18 [ ] 127 | 2 -> 18 [ ] 128 | 7 -> 18 [ ] 129 | 27 -> 18 [ ] 130 | 25 -> 18 [ ] 131 | 26 -> 2 [ ] 132 | 15 -> 2 [ ] 133 | 4 -> 2 [ ] 134 | 16 -> 2 [ ] 135 | 25 -> 2 [ ] 136 | 1 -> 26 [ ] 137 | 15 -> 19 [ ] 138 | 7 -> 19 [ ] 139 | 22 -> 19 [ ] 140 | 23 -> 22 [ ] 141 | 22 -> 22 [ ] 142 | 23 -> 23 [ ] 143 | 11 -> 23 [ ] 144 | 14 -> 23 [ ] 145 | 24 -> 14 [ ] 146 | 12 -> 0 [ ] 147 | 12 -> 14 [ ] 148 | 11 -> 15 [ ] 149 | 14 -> 15 [ ] 150 | 10 -> 15 [ ] 151 | 0 -> 7 [ ] 152 | 15 -> 7 [ ] 153 | 14 -> 7 [ ] 154 | 26 -> 5 [ ] 155 | 3 -> 27 [ ] 156 | 2 -> 27 [ ] 157 | 25 -> 27 [ ] 158 | 14 -> 27 [ ] 159 | } 160 | ``` 161 | 162 | ![graph](graph.svg) 163 | 164 | ### Filter nodes by GraphQL types(s) 165 | 166 | ```sh 167 | craftql tests/fixtures --filter object object_extension 168 | 169 | digraph { 170 | 0 [ label = "Orphan (Object)\l\l[ID]" ] 171 | 1 [ label = "Human (Object)\l\l[Character, Episode, Float, FriendsConnection, ID, Int, LengthUnit, Starship, String]" ] 172 | 2 [ label = "Droid (Object)\l\l[Character, Episode, FriendsConnection, ID, Int, String]" ] 173 | 3 [ label = "FriendsConnection (Object)\l\l[Character, FriendsEdge, Int, PageInfo]" ] 174 | 4 [ label = "FriendsEdge (Object)\l\l[Character, ID]" ] 175 | 5 [ label = "PageInfo (Object)\l\l[@test, Boolean, ID]" ] 176 | 6 [ label = "Review (Object)\l\l[@test, DateTime, Episode, Int, String]" ] 177 | 7 [ label = "Query (Object)\l\l[Character, Droid, Episode, Human, ID, Review, SearchResult, Starship, String]" ] 178 | 8 [ label = "Mutation (Object)\l\l[Episode, Review, ReviewInput]" ] 179 | 9 [ label = "Subscription (Object)\l\l[Episode, Review]" ] 180 | 10 [ label = "Starship (Object extension)\l\l[Boolean, Starship]" ] 181 | 11 [ label = "Starship (Object)\l\l[@deprecated, Float, ID, LengthUnit, String]" ] 182 | 4 -> 3 [ ] 183 | 5 -> 3 [ ] 184 | 10 -> 11 [ ] 185 | 2 -> 7 [ ] 186 | 1 -> 7 [ ] 187 | 6 -> 7 [ ] 188 | 11 -> 7 [ ] 189 | 6 -> 9 [ ] 190 | 3 -> 2 [ ] 191 | 6 -> 8 [ ] 192 | 3 -> 1 [ ] 193 | 11 -> 1 [ ] 194 | } 195 | ``` 196 | 197 | ### Find and display one node 198 | 199 | ```sh 200 | craftql tests/fixtures --node Character 201 | 202 | # tests/fixtures/Types/Interfaces/Character.graphql 203 | interface Character @test { 204 | id: ID! 205 | name: String! 206 | friends: [Character] 207 | friendsConnection(first: Int, after: ID): FriendsConnection! 208 | appearsIn: [Episode]! 209 | cute: Bool! @deprecated 210 | } 211 | ``` 212 | 213 | ### Find and display multiple nodes 214 | 215 | ```sh 216 | craftql tests/fixtures --nodes Character Episode 217 | 218 | # tests/fixtures/Types/Interfaces/Character.graphql 219 | interface Character @test { 220 | id: ID! 221 | name: String! 222 | friends: [Character] 223 | friendsConnection(first: Int, after: ID): FriendsConnection! 224 | appearsIn: [Episode]! 225 | cute: Bool! @deprecated 226 | } 227 | 228 | 229 | # tests/fixtures/Types/Enums/Episode.gql 230 | enum Episode @test(letter: B) { 231 | NEWHOPE @deprecated 232 | EMPIRE 233 | JEDI 234 | } 235 | ``` 236 | 237 | ### Find and display orphan(s) node(s) 238 | 239 | ```sh 240 | craftql tests/fixtures --orphans 241 | 242 | # tests/fixtures/Types/Types/orphan.gql 243 | type Orphan { 244 | id: ID! 245 | } 246 | ``` 247 | 248 | ### Find and display incoming dependencies of a node 249 | 250 | ```sh 251 | craftql tests/fixtures --incoming-dependencies Starship 252 | 253 | # tests/fixtures/Types/Types/extension.graphql 254 | extend type Starship { 255 | antiGravity: Boolean! 256 | } 257 | 258 | 259 | # tests/fixtures/Types/Enums/LengthUnit.graphql 260 | enum LengthUnit { 261 | METER 262 | FOOT 263 | } 264 | 265 | 266 | # tests/fixtures/Directives/deprecated.graphql 267 | directive @deprecated(reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE 268 | ``` 269 | 270 | ### Find and display outgoing dependencies of a node 271 | 272 | ```sh 273 | craftql tests/fixtures --outgoing-dependencies Starship 274 | 275 | # tests/fixtures/Types/Types/b.graphql 276 | type Human implements Character { 277 | id: ID! 278 | name: String! 279 | homePlanet: String 280 | height(unit: LengthUnit = METER): Float 281 | mass: Float 282 | friends: [Character] 283 | friendsConnection(first: Int, after: ID): FriendsConnection! 284 | appearsIn: [Episode]! 285 | starships: [Starship] 286 | } 287 | 288 | 289 | # tests/fixtures/Types/Unions/SearchResults.graphql 290 | union SearchResult @test = Human | Droid | Starship 291 | 292 | 293 | # tests/fixtures/Types/Types/a.gql 294 | type Query { 295 | hero(episode: Episode): Character 296 | reviews(episode: Episode!): [Review] 297 | search(text: String): [SearchResult] 298 | character(id: ID!): Character 299 | droid(id: ID!): Droid 300 | human(id: ID!): Human 301 | starship(id: ID!): Starship 302 | } 303 | ``` 304 | 305 | ### Find and display missing definition(s) 306 | 307 | ```sh 308 | craftql tests/fixtures --orphans 309 | 310 | # Color is not defined in: 311 | # tests/fixtures/Types/Interfaces/Character.graphql 312 | interface Character @test { 313 | id: ID! 314 | name: String! 315 | friends: [Character] 316 | friendsConnection(first: Int, after: ID): FriendsConnection! 317 | appearsIn: [Episode]! 318 | cute: Boolean! @deprecated 319 | preferedColor: Color 320 | } 321 | 322 | 323 | # Ewok, Gungan are not defined in: 324 | # tests/fixtures/Types/Unions/SearchResultExtension.graphql 325 | extend union SearchResult = Ewok | Gungan 326 | ``` 327 | -------------------------------------------------------------------------------- /graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %0 5 | 6 | 7 | 8 | 0 9 | 10 | Query (Object) 11 | [Character, Droid, Episode, Human, ID, Review, SearchResult, Starship, String] 12 | 13 | 14 | 15 | 11 16 | 17 | schema (Schema) 18 | [Mutation, Query, Subscription] 19 | 20 | 21 | 22 | 0->11 23 | 24 | 25 | 26 | 27 | 28 | 1 29 | 30 | Mutation (Object) 31 | [Episode, Review, ReviewInput] 32 | 33 | 34 | 35 | 1->11 36 | 37 | 38 | 39 | 40 | 41 | 2 42 | 43 | Subscription (Object) 44 | [Episode, Review] 45 | 46 | 47 | 48 | 2->11 49 | 50 | 51 | 52 | 53 | 54 | 3 55 | 56 | Character (Interface) 57 | [@deprecated, @test, Bool, Character, Episode, FriendsConnection, ID, Int, String] 58 | 59 | 60 | 61 | 3->0 62 | 63 | 64 | 65 | 66 | 67 | 3->3 68 | 69 | 70 | 71 | 72 | 73 | 13 74 | 75 | Human (Object) 76 | [Character, Episode, Float, FriendsConnection, ID, Int, LengthUnit, Starship, String] 77 | 78 | 79 | 80 | 3->13 81 | 82 | 83 | 84 | 85 | 86 | 14 87 | 88 | Droid (Object) 89 | [Character, Episode, FriendsConnection, ID, Int, String] 90 | 91 | 92 | 93 | 3->14 94 | 95 | 96 | 97 | 98 | 99 | 15 100 | 101 | FriendsConnection (Object) 102 | [Character, FriendsEdge, Int, PageInfo] 103 | 104 | 105 | 106 | 3->15 107 | 108 | 109 | 110 | 111 | 112 | 16 113 | 114 | FriendsEdge (Object) 115 | [Character, ID] 116 | 117 | 118 | 119 | 3->16 120 | 121 | 122 | 123 | 124 | 125 | 4 126 | 127 | Starship (Object) 128 | [@deprecated, Float, ID, LengthUnit, String] 129 | 130 | 131 | 132 | 4->0 133 | 134 | 135 | 136 | 137 | 138 | 4->13 139 | 140 | 141 | 142 | 143 | 144 | 24 145 | 146 | SearchResult (Union) 147 | [@test, Droid, Human, Starship] 148 | 149 | 150 | 151 | 4->24 152 | 153 | 154 | 155 | 156 | 157 | 5 158 | 159 | @deprecated (Directive) 160 | [String] 161 | 162 | 163 | 164 | 5->3 165 | 166 | 167 | 168 | 169 | 170 | 5->4 171 | 172 | 173 | 174 | 175 | 176 | 9 177 | 178 | ColorInput (InputObject) 179 | [@deprecated, @test, ColorInput, Int] 180 | 181 | 182 | 183 | 5->9 184 | 185 | 186 | 187 | 188 | 189 | 21 190 | 191 | Episode (Enum) 192 | [@deprecated, @test] 193 | 194 | 195 | 196 | 5->21 197 | 198 | 199 | 200 | 201 | 202 | 6 203 | 204 | SearchResult (Union extension) 205 | [Ewok, SearchResult] 206 | 207 | 208 | 209 | 6->24 210 | 211 | 212 | 213 | 214 | 215 | 7 216 | 217 | LengthUnit (Enum) 218 | 219 | 220 | 221 | 7->4 222 | 223 | 224 | 225 | 226 | 227 | 7->13 228 | 229 | 230 | 231 | 232 | 233 | 8 234 | 235 | ReviewInput (InputObject) 236 | [ColorInput, Int, ReviewInput, String] 237 | 238 | 239 | 240 | 8->1 241 | 242 | 243 | 244 | 245 | 246 | 8->8 247 | 248 | 249 | 250 | 251 | 252 | 9->8 253 | 254 | 255 | 256 | 257 | 258 | 9->9 259 | 260 | 261 | 262 | 263 | 264 | 10 265 | 266 | Character (Interface extension) 267 | [Boolean, Character] 268 | 269 | 270 | 271 | 10->3 272 | 273 | 274 | 275 | 276 | 277 | 12 278 | 279 | Letter (Enum) 280 | 281 | 282 | 283 | 25 284 | 285 | @test (Directive) 286 | [Letter] 287 | 288 | 289 | 290 | 12->25 291 | 292 | 293 | 294 | 295 | 296 | 13->0 297 | 298 | 299 | 300 | 301 | 302 | 13->24 303 | 304 | 305 | 306 | 307 | 308 | 14->0 309 | 310 | 311 | 312 | 313 | 314 | 14->24 315 | 316 | 317 | 318 | 319 | 320 | 15->3 321 | 322 | 323 | 324 | 325 | 326 | 15->13 327 | 328 | 329 | 330 | 331 | 332 | 15->14 333 | 334 | 335 | 336 | 337 | 338 | 16->15 339 | 340 | 341 | 342 | 343 | 344 | 17 345 | 346 | PageInfo (Object) 347 | [@test, Boolean, ID] 348 | 349 | 350 | 351 | 17->15 352 | 353 | 354 | 355 | 356 | 357 | 18 358 | 359 | Review (Object) 360 | [@test, DateTime, Episode, Int, String] 361 | 362 | 363 | 364 | 18->0 365 | 366 | 367 | 368 | 369 | 370 | 18->1 371 | 372 | 373 | 374 | 375 | 376 | 18->2 377 | 378 | 379 | 380 | 381 | 382 | 19 383 | 384 | DateTime (Scalar extension) 385 | [@test, DateTime] 386 | 387 | 388 | 389 | 19->25 390 | 391 | 392 | 393 | 394 | 395 | 27 396 | 397 | DateTime (Scalar) 398 | 399 | 400 | 401 | 19->27 402 | 403 | 404 | 405 | 406 | 407 | 20 408 | 409 | Orphan (Object) 410 | [ID] 411 | 412 | 413 | 414 | 21->0 415 | 416 | 417 | 418 | 419 | 420 | 21->1 421 | 422 | 423 | 424 | 425 | 426 | 21->2 427 | 428 | 429 | 430 | 431 | 432 | 21->3 433 | 434 | 435 | 436 | 437 | 438 | 21->13 439 | 440 | 441 | 442 | 443 | 444 | 21->14 445 | 446 | 447 | 448 | 449 | 450 | 21->18 451 | 452 | 453 | 454 | 455 | 456 | 22 457 | 458 | ColorInput (InputObject extension) 459 | [ColorInput, Int] 460 | 461 | 462 | 463 | 22->9 464 | 465 | 466 | 467 | 468 | 469 | 23 470 | 471 | Episode (Enum extension) 472 | [Episode] 473 | 474 | 475 | 476 | 23->21 477 | 478 | 479 | 480 | 481 | 482 | 24->0 483 | 484 | 485 | 486 | 487 | 488 | 25->3 489 | 490 | 491 | 492 | 493 | 494 | 25->9 495 | 496 | 497 | 498 | 499 | 500 | 25->17 501 | 502 | 503 | 504 | 505 | 506 | 25->18 507 | 508 | 509 | 510 | 511 | 512 | 25->21 513 | 514 | 515 | 516 | 517 | 518 | 25->24 519 | 520 | 521 | 522 | 523 | 524 | 26 525 | 526 | Starship (Object extension) 527 | [Boolean, Starship] 528 | 529 | 530 | 531 | 26->4 532 | 533 | 534 | 535 | 536 | 537 | 27->18 538 | 539 | 540 | 541 | 542 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | -------------------------------------------------------------------------------- /src/bin.rs: -------------------------------------------------------------------------------- 1 | #![deny(unsafe_code, nonstandard_style)] 2 | 3 | use anyhow::Result; 4 | use async_std::path::PathBuf; 5 | use clap::Parser; 6 | use craftql::{ 7 | state::{GraphQL, State}, 8 | utils::{ 9 | find_and_print_neighbors, find_and_print_orphans, find_node, get_files, 10 | populate_graph_from_ast, print_missing_definitions, 11 | }, 12 | }; 13 | use petgraph::{ 14 | dot::{Config, Dot}, 15 | Direction, 16 | }; 17 | 18 | #[derive(Parser)] 19 | #[clap(about, author, version)] 20 | struct Opts { 21 | /// Path to get files from 22 | path: PathBuf, 23 | 24 | /// Finds and displays incoming dependencies of a node 25 | #[clap(short, long)] 26 | incoming_dependencies: Option, 27 | 28 | /// Finds and displays missing definition(s) 29 | #[clap(short, long)] 30 | missing_definitions: bool, 31 | 32 | /// Finds and displays orphan(s) node(s) 33 | #[clap(short = 'O', long)] 34 | orphans: bool, 35 | 36 | /// Finds and displays outgoing dependencies of a node 37 | #[clap(short, long)] 38 | outgoing_dependencies: Option, 39 | 40 | /// Finds and displays one node 41 | #[clap(short, long)] 42 | node: Option, 43 | 44 | /// Finds and displays multiple nodes 45 | #[clap(short = 'N', long)] 46 | nodes: Vec, 47 | 48 | /// Filter nodes by GraphQL type(s) 49 | /// 50 | /// - directive 51 | /// - enum 52 | /// - enum_extension 53 | /// - input_object 54 | /// - input_object_extension 55 | /// - interface 56 | /// - interface_extension 57 | /// - object 58 | /// - object_extension 59 | /// - scalar 60 | /// - scalar_extension 61 | /// - schema 62 | /// - union 63 | /// - union_extension 64 | #[clap(short, long, verbatim_doc_comment)] 65 | filter: Vec, 66 | } 67 | 68 | #[async_std::main] 69 | async fn main() -> Result<()> { 70 | let opts: Opts = Opts::parse(); 71 | let state = State::default(); 72 | let shared_data = state.shared; 73 | let shared_data_for_populate = shared_data.clone(); 74 | 75 | // Walk the GraphQL files and populate the data. 76 | get_files(opts.path, shared_data.files).await?; 77 | 78 | // Populate the graph. 79 | populate_graph_from_ast( 80 | shared_data_for_populate.dependencies, 81 | shared_data_for_populate.files, 82 | &opts.filter, 83 | shared_data_for_populate.graph, 84 | shared_data_for_populate.missing_definitions, 85 | ) 86 | .await?; 87 | 88 | if let Some(ref node) = opts.incoming_dependencies { 89 | find_and_print_neighbors(node, shared_data.graph.clone(), Direction::Incoming).await?; 90 | 91 | return Ok(()); 92 | } 93 | 94 | if let Some(ref node) = opts.outgoing_dependencies { 95 | find_and_print_neighbors(node, shared_data.graph.clone(), Direction::Outgoing).await?; 96 | 97 | return Ok(()); 98 | } 99 | 100 | if let Some(ref node) = opts.node { 101 | find_node(node, shared_data.graph.clone()).await?; 102 | 103 | return Ok(()); 104 | } 105 | 106 | if !opts.nodes.is_empty() { 107 | for ref node in opts.nodes { 108 | find_node(node, shared_data.graph.clone()).await?; 109 | } 110 | 111 | return Ok(()); 112 | } 113 | 114 | if opts.missing_definitions { 115 | print_missing_definitions( 116 | shared_data.graph.clone(), 117 | shared_data.missing_definitions.clone(), 118 | ) 119 | .await?; 120 | 121 | return Ok(()); 122 | } 123 | 124 | if opts.orphans { 125 | find_and_print_orphans(shared_data.graph.clone()).await?; 126 | 127 | return Ok(()); 128 | } 129 | 130 | // Render the graph without edges. 131 | let graph = &*shared_data.graph.lock().await; 132 | println!("\n{:?}", Dot::with_config(&graph, &[Config::EdgeNoLabel])); 133 | 134 | Ok(()) 135 | } 136 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | /// List of allowed file extensions. 2 | pub const ALLOWED_EXTENSIONS: [&str; 2] = ["graphql", "gql"]; 3 | -------------------------------------------------------------------------------- /src/extend_types.rs: -------------------------------------------------------------------------------- 1 | use crate::state::{GraphQL, GraphQLType}; 2 | 3 | use graphql_parser::schema; 4 | 5 | /// Convert Text to String. 6 | /// See https://github.com/graphql-rust/graphql-parser/blob/master/src/common.rs#L12-L28 7 | fn convert_text_to_string<'a, T>(text: &T::Value) -> String 8 | where 9 | T: schema::Text<'a>, 10 | { 11 | String::from(text.as_ref()) 12 | } 13 | 14 | /// Convert text to directive identifier. 15 | fn convert_text_to_directive<'a, T>(text: &T::Value) -> String 16 | where 17 | T: schema::Text<'a>, 18 | { 19 | format!("@{}", convert_text_to_string::(text)) 20 | } 21 | 22 | /// Extend id for type extensions. 23 | /// Only used internally to distinguish between a type and its extension. 24 | fn get_extended_id(id: String) -> String { 25 | format!("{}__", id) 26 | } 27 | 28 | /// Extract dependencies from any entity's directives. 29 | fn get_dependencies_from_directives<'a, T>(directives: &[schema::Directive<'a, T>]) -> Vec 30 | where 31 | T: schema::Text<'a>, 32 | { 33 | directives 34 | .iter() 35 | .map(|directive| convert_text_to_directive::(&directive.name)) 36 | .collect::>() 37 | } 38 | 39 | fn sort_and_dedupe_dependencies(mut dependencies: Vec) -> Vec { 40 | dependencies.sort_by_key(|a| a.to_lowercase()); 41 | dependencies.dedup(); 42 | dependencies 43 | } 44 | 45 | /// Recursively walk a field to get the dependencies. 46 | fn walk_field<'a, T>(field: &schema::Field<'a, T>) -> Vec 47 | where 48 | T: schema::Text<'a>, 49 | { 50 | field 51 | // Inject arguments. 52 | .arguments 53 | .iter() 54 | .map(|argument| walk_field_type(&argument.value_type)) 55 | // Inject directives. 56 | .chain(get_dependencies_from_directives(&field.directives)) 57 | // Inject field type. 58 | .chain(vec![walk_field_type(&field.field_type)]) 59 | .collect::>() 60 | } 61 | 62 | /// Recursively walk a field type to get the inner String value. 63 | fn walk_field_type<'a, T>(field_type: &schema::Type<'a, T>) -> String 64 | where 65 | T: schema::Text<'a>, 66 | { 67 | match field_type { 68 | schema::Type::NamedType(name) => convert_text_to_string::(name), 69 | schema::Type::ListType(field_type) => { 70 | // Field type is boxed, need to unbox. 71 | walk_field_type(field_type.as_ref()) 72 | } 73 | schema::Type::NonNullType(field_type) => { 74 | // Same here. 75 | walk_field_type(field_type.as_ref()) 76 | } 77 | } 78 | } 79 | 80 | /// Recursively walk an input to get the dependencies. 81 | fn walk_input_value<'a, T>(input_value: &schema::InputValue<'a, T>) -> Vec 82 | where 83 | T: schema::Text<'a>, 84 | { 85 | get_dependencies_from_directives(&input_value.directives) 86 | .into_iter() 87 | .chain(vec![walk_field_type(&input_value.value_type)]) 88 | .collect::>() 89 | } 90 | 91 | /// Trait providing extension methods for graphql_parser::schema. 92 | pub trait ExtendType { 93 | /// Method to get the dependencies. 94 | fn get_dependencies(&self) -> Vec; 95 | /// Method to get id and the name, id is optional and can be copied from name. 96 | fn get_id_and_name(&self) -> (Option, String); 97 | /// Method to get the internal GraphQL mapped type. 98 | fn get_mapped_type(&self) -> GraphQL; 99 | /// Method to get the raw representation. 100 | fn get_raw(&self) -> String; 101 | } 102 | 103 | impl<'a, T> ExtendType for schema::TypeDefinition<'a, T> 104 | where 105 | T: schema::Text<'a>, 106 | { 107 | fn get_dependencies(&self) -> Vec { 108 | match self { 109 | schema::TypeDefinition::Enum(enum_type) => { 110 | sort_and_dedupe_dependencies( 111 | // Get root directives. 112 | get_dependencies_from_directives(&enum_type.directives) 113 | .into_iter() 114 | // Get values' directives. 115 | .chain(enum_type.values.iter().flat_map(|enum_value| { 116 | get_dependencies_from_directives(&enum_value.directives) 117 | })) 118 | .collect::>(), 119 | ) 120 | } 121 | schema::TypeDefinition::Scalar(scalar_type) => { 122 | sort_and_dedupe_dependencies( 123 | // Get root directives. 124 | get_dependencies_from_directives(&scalar_type.directives), 125 | ) 126 | } 127 | schema::TypeDefinition::Object(object_type) => { 128 | sort_and_dedupe_dependencies( 129 | // Get fields' dependencies. 130 | object_type 131 | .fields 132 | .iter() 133 | .flat_map(|field| walk_field(field)) 134 | // Get root directives. 135 | .chain(get_dependencies_from_directives(&object_type.directives)) 136 | // Get interfaces as dependencies. 137 | .chain( 138 | object_type 139 | .implements_interfaces 140 | .iter() 141 | .map(|directive| convert_text_to_string::(directive)), 142 | ) 143 | .collect::>(), 144 | ) 145 | } 146 | schema::TypeDefinition::Interface(interface_type) => { 147 | sort_and_dedupe_dependencies( 148 | // Get fields' dependencies. 149 | interface_type 150 | .fields 151 | .iter() 152 | .flat_map(|field| walk_field(field)) 153 | // Get root directives. 154 | .chain(get_dependencies_from_directives(&interface_type.directives)) 155 | .collect::>(), 156 | ) 157 | } 158 | schema::TypeDefinition::Union(union_type) => { 159 | sort_and_dedupe_dependencies( 160 | // Get types as dependencies. 161 | union_type 162 | .types 163 | .iter() 164 | .map(|inner_type| convert_text_to_string::(inner_type)) 165 | // Get root directives. 166 | .chain(get_dependencies_from_directives(&union_type.directives)) 167 | .collect::>(), 168 | ) 169 | } 170 | schema::TypeDefinition::InputObject(input_object_type) => { 171 | sort_and_dedupe_dependencies( 172 | // Get fields' dependencies. 173 | input_object_type 174 | .fields 175 | .iter() 176 | .flat_map(|input_value| walk_input_value(input_value)) 177 | // Get root directives. 178 | .chain(get_dependencies_from_directives( 179 | &input_object_type.directives, 180 | )) 181 | .collect::>(), 182 | ) 183 | } 184 | } 185 | } 186 | fn get_id_and_name(&self) -> (Option, String) { 187 | ( 188 | None, 189 | convert_text_to_string::(match self { 190 | schema::TypeDefinition::Enum(enum_type) => &enum_type.name, 191 | schema::TypeDefinition::Scalar(scalar_type) => &scalar_type.name, 192 | schema::TypeDefinition::Object(object_type) => &object_type.name, 193 | schema::TypeDefinition::Interface(interface_type) => &interface_type.name, 194 | schema::TypeDefinition::Union(union_type) => &union_type.name, 195 | schema::TypeDefinition::InputObject(input_object_type) => &input_object_type.name, 196 | }), 197 | ) 198 | } 199 | fn get_mapped_type(&self) -> GraphQL { 200 | match self { 201 | schema::TypeDefinition::Enum(_) => GraphQL::TypeDefinition(GraphQLType::Enum), 202 | schema::TypeDefinition::Scalar(_) => GraphQL::TypeDefinition(GraphQLType::Scalar), 203 | schema::TypeDefinition::Object(_) => GraphQL::TypeDefinition(GraphQLType::Object), 204 | schema::TypeDefinition::Interface(_) => GraphQL::TypeDefinition(GraphQLType::Interface), 205 | schema::TypeDefinition::Union(_) => GraphQL::TypeDefinition(GraphQLType::Union), 206 | schema::TypeDefinition::InputObject(_) => { 207 | GraphQL::TypeDefinition(GraphQLType::InputObject) 208 | } 209 | } 210 | } 211 | fn get_raw(&self) -> String { 212 | match self { 213 | schema::TypeDefinition::Enum(enum_type) => enum_type.to_string(), 214 | schema::TypeDefinition::Scalar(scalar_type) => scalar_type.to_string(), 215 | schema::TypeDefinition::Object(object_type) => object_type.to_string(), 216 | schema::TypeDefinition::Interface(interface_type) => interface_type.to_string(), 217 | schema::TypeDefinition::Union(union_type) => union_type.to_string(), 218 | schema::TypeDefinition::InputObject(input_object_type) => input_object_type.to_string(), 219 | } 220 | } 221 | } 222 | 223 | impl<'a, T> ExtendType for schema::TypeExtension<'a, T> 224 | where 225 | T: schema::Text<'a>, 226 | { 227 | fn get_dependencies(&self) -> Vec { 228 | match self { 229 | schema::TypeExtension::Enum(enum_type_extension) => { 230 | sort_and_dedupe_dependencies( 231 | // Get root directives. 232 | get_dependencies_from_directives(&enum_type_extension.directives) 233 | .into_iter() 234 | // Get values' directives. 235 | .chain(enum_type_extension.values.iter().flat_map(|enum_value| { 236 | get_dependencies_from_directives(&enum_value.directives) 237 | })) 238 | // Add extension's source. 239 | .chain(vec![convert_text_to_string::(&enum_type_extension.name)]) 240 | .collect::>(), 241 | ) 242 | } 243 | schema::TypeExtension::Scalar(scalar_type_extension) => { 244 | sort_and_dedupe_dependencies( 245 | // Get root directives. 246 | get_dependencies_from_directives(&scalar_type_extension.directives) 247 | .into_iter() 248 | // Add extension's source. 249 | .chain(vec![convert_text_to_string::( 250 | &scalar_type_extension.name, 251 | )]) 252 | .collect::>(), 253 | ) 254 | } 255 | schema::TypeExtension::Object(object_type_extension) => { 256 | sort_and_dedupe_dependencies( 257 | // Get fields' dependencies. 258 | object_type_extension 259 | .fields 260 | .iter() 261 | .flat_map(|field| walk_field(field)) 262 | // Get root directives. 263 | .chain(get_dependencies_from_directives( 264 | &object_type_extension.directives, 265 | )) 266 | // Get interfaces as dependencies. 267 | .chain( 268 | object_type_extension 269 | .implements_interfaces 270 | .iter() 271 | .map(|directive| convert_text_to_string::(directive)), 272 | ) 273 | // Add extension's source. 274 | .chain(vec![String::from(object_type_extension.name.as_ref())]) 275 | .collect::>(), 276 | ) 277 | } 278 | schema::TypeExtension::Interface(interface_type_extension) => { 279 | sort_and_dedupe_dependencies( 280 | // Get fields' dependencies. 281 | interface_type_extension 282 | .fields 283 | .iter() 284 | .flat_map(|field| walk_field(field)) 285 | // Get root directives. 286 | .chain(get_dependencies_from_directives( 287 | &interface_type_extension.directives, 288 | )) 289 | // Add extension's source. 290 | .chain(vec![convert_text_to_string::( 291 | &interface_type_extension.name, 292 | )]) 293 | .collect::>(), 294 | ) 295 | } 296 | schema::TypeExtension::Union(union_type_extension) => { 297 | sort_and_dedupe_dependencies( 298 | // Get types as dependencies. 299 | union_type_extension 300 | .types 301 | .iter() 302 | .map(|inner_type| convert_text_to_string::(inner_type)) 303 | // Get root directives. 304 | .chain(get_dependencies_from_directives( 305 | &union_type_extension.directives, 306 | )) 307 | // Add extension's source. 308 | .chain(vec![convert_text_to_string::( 309 | &union_type_extension.name, 310 | )]) 311 | .collect::>(), 312 | ) 313 | } 314 | schema::TypeExtension::InputObject(input_object_type_extension) => { 315 | sort_and_dedupe_dependencies( 316 | // Get fields' dependencies. 317 | input_object_type_extension 318 | .fields 319 | .iter() 320 | .flat_map(|input_value| walk_input_value(input_value)) 321 | // Get root directives. 322 | .chain(get_dependencies_from_directives( 323 | &input_object_type_extension.directives, 324 | )) 325 | // Add extension's source. 326 | .chain(vec![convert_text_to_string::( 327 | &input_object_type_extension.name, 328 | )]) 329 | .collect::>(), 330 | ) 331 | } 332 | } 333 | } 334 | fn get_id_and_name(&self) -> (Option, String) { 335 | let name = convert_text_to_string::(match self { 336 | schema::TypeExtension::Enum(enum_type_extension) => &enum_type_extension.name, 337 | schema::TypeExtension::Scalar(scalar_type_extension) => &scalar_type_extension.name, 338 | schema::TypeExtension::Object(object_type_extension) => &object_type_extension.name, 339 | schema::TypeExtension::Interface(interface_type_extension) => { 340 | &interface_type_extension.name 341 | } 342 | schema::TypeExtension::Union(union_type_extension) => &union_type_extension.name, 343 | schema::TypeExtension::InputObject(input_object_type) => &input_object_type.name, 344 | }); 345 | 346 | (Some(get_extended_id(name.clone())), name) 347 | } 348 | fn get_mapped_type(&self) -> GraphQL { 349 | match self { 350 | schema::TypeExtension::Enum(_) => GraphQL::TypeExtension(GraphQLType::Enum), 351 | schema::TypeExtension::Scalar(_) => GraphQL::TypeExtension(GraphQLType::Scalar), 352 | schema::TypeExtension::Object(_) => GraphQL::TypeExtension(GraphQLType::Object), 353 | schema::TypeExtension::Interface(_) => GraphQL::TypeExtension(GraphQLType::Interface), 354 | schema::TypeExtension::Union(_) => GraphQL::TypeExtension(GraphQLType::Union), 355 | schema::TypeExtension::InputObject(_) => { 356 | GraphQL::TypeExtension(GraphQLType::InputObject) 357 | } 358 | } 359 | } 360 | fn get_raw(&self) -> String { 361 | match self { 362 | schema::TypeExtension::Enum(enum_type) => enum_type.to_string(), 363 | schema::TypeExtension::Scalar(scalar_type) => scalar_type.to_string(), 364 | schema::TypeExtension::Object(object_type) => object_type.to_string(), 365 | schema::TypeExtension::Interface(interface_type) => interface_type.to_string(), 366 | schema::TypeExtension::Union(union_type) => union_type.to_string(), 367 | schema::TypeExtension::InputObject(input_object_type) => input_object_type.to_string(), 368 | } 369 | } 370 | } 371 | 372 | impl<'a, T> ExtendType for schema::SchemaDefinition<'a, T> 373 | where 374 | T: schema::Text<'a>, 375 | { 376 | fn get_dependencies(&self) -> Vec { 377 | sort_and_dedupe_dependencies( 378 | // A schema can only have a query, a mutation and a subscription. 379 | vec![&self.query, &self.mutation, &self.subscription] 380 | .into_iter() 381 | .filter_map(|field| { 382 | field 383 | .as_ref() 384 | .map(|field| convert_text_to_string::(field)) 385 | }) 386 | .collect::>(), 387 | ) 388 | } 389 | fn get_id_and_name(&self) -> (Option, String) { 390 | // A Schema has no name, use a default one. 391 | (None, String::from("schema")) 392 | } 393 | fn get_mapped_type(&self) -> GraphQL { 394 | GraphQL::Schema 395 | } 396 | fn get_raw(&self) -> String { 397 | self.to_string() 398 | } 399 | } 400 | 401 | impl<'a, T> ExtendType for schema::DirectiveDefinition<'a, T> 402 | where 403 | T: schema::Text<'a>, 404 | { 405 | fn get_dependencies(&self) -> Vec { 406 | sort_and_dedupe_dependencies( 407 | self.arguments 408 | .iter() 409 | .flat_map(|input_value| walk_input_value(input_value)) 410 | .collect::>(), 411 | ) 412 | } 413 | fn get_id_and_name(&self) -> (Option, String) { 414 | let name = convert_text_to_directive::(&self.name); 415 | (None, name) 416 | } 417 | fn get_mapped_type(&self) -> GraphQL { 418 | GraphQL::Directive 419 | } 420 | fn get_raw(&self) -> String { 421 | self.to_string() 422 | } 423 | } 424 | 425 | #[cfg(test)] 426 | mod tests { 427 | use super::*; 428 | 429 | use graphql_parser::parse_schema; 430 | 431 | fn match_and_assert( 432 | contents: &str, 433 | dependencies: Vec<&str>, 434 | id_and_name: (Option, String), 435 | mapped_type: GraphQL, 436 | ) { 437 | fn assert( 438 | schema_type: impl ExtendType, 439 | dependencies: Vec<&str>, 440 | id_and_name: (Option, String), 441 | mapped_type: GraphQL, 442 | raw: String, 443 | ) { 444 | assert_eq!(schema_type.get_dependencies(), dependencies); 445 | assert_eq!(schema_type.get_id_and_name(), id_and_name); 446 | assert_eq!(schema_type.get_mapped_type(), mapped_type); 447 | assert_eq!(schema_type.get_raw(), raw); 448 | } 449 | 450 | let document = parse_schema::(contents).unwrap().to_owned(); 451 | 452 | match document.definitions.get(0).unwrap().to_owned() { 453 | schema::Definition::TypeDefinition(type_definition) => assert( 454 | type_definition, 455 | dependencies, 456 | id_and_name, 457 | mapped_type, 458 | document.to_string(), 459 | ), 460 | schema::Definition::TypeExtension(type_extension) => assert( 461 | type_extension, 462 | dependencies, 463 | id_and_name, 464 | mapped_type, 465 | document.to_string(), 466 | ), 467 | schema::Definition::SchemaDefinition(schema_definition) => assert( 468 | schema_definition, 469 | dependencies, 470 | id_and_name, 471 | mapped_type, 472 | document.to_string(), 473 | ), 474 | schema::Definition::DirectiveDefinition(directive_definition) => assert( 475 | directive_definition, 476 | dependencies, 477 | id_and_name, 478 | mapped_type, 479 | document.to_string(), 480 | ), 481 | }; 482 | } 483 | 484 | #[test] 485 | fn test_enum() { 486 | match_and_assert( 487 | "enum Foo @foo { A @bar B C}", 488 | vec!["@bar", "@foo"], 489 | (None, String::from("Foo")), 490 | GraphQL::TypeDefinition(GraphQLType::Enum), 491 | ); 492 | } 493 | 494 | #[test] 495 | fn test_extend_enum() { 496 | match_and_assert( 497 | "extend enum Foo @foo { D @bar }", 498 | vec!["@bar", "@foo", "Foo"], 499 | (Some(String::from("Foo__")), String::from("Foo")), 500 | GraphQL::TypeExtension(GraphQLType::Enum), 501 | ); 502 | } 503 | 504 | #[test] 505 | fn test_input_object() { 506 | match_and_assert( 507 | "input Foo @test { bar: Int! @deprecated }", 508 | vec!["@deprecated", "@test", "Int"], 509 | (None, String::from("Foo")), 510 | GraphQL::TypeDefinition(GraphQLType::InputObject), 511 | ); 512 | } 513 | 514 | #[test] 515 | fn test_extend_input_object() { 516 | match_and_assert( 517 | "extend input Foo @test { woot: Int! @deprecated }", 518 | vec!["@deprecated", "@test", "Foo", "Int"], 519 | (Some(String::from("Foo__")), String::from("Foo")), 520 | GraphQL::TypeExtension(GraphQLType::InputObject), 521 | ); 522 | } 523 | 524 | #[test] 525 | fn test_interface() { 526 | match_and_assert( 527 | "interface Foo @test { id: ID! @deprecated }", 528 | vec!["@deprecated", "@test", "ID"], 529 | (None, String::from("Foo")), 530 | GraphQL::TypeDefinition(GraphQLType::Interface), 531 | ); 532 | } 533 | 534 | #[test] 535 | fn test_extend_inteface() { 536 | match_and_assert( 537 | "extend interface Foo @test { woot: String! @deprecated }", 538 | vec!["@deprecated", "@test", "Foo", "String"], 539 | (Some(String::from("Foo__")), String::from("Foo")), 540 | GraphQL::TypeExtension(GraphQLType::Interface), 541 | ); 542 | } 543 | 544 | #[test] 545 | fn test_object() { 546 | match_and_assert( 547 | "type Foo implements Bar @test { id: ID! @skip }", 548 | vec!["@skip", "@test", "Bar", "ID"], 549 | (None, String::from("Foo")), 550 | GraphQL::TypeDefinition(GraphQLType::Object), 551 | ); 552 | } 553 | 554 | #[test] 555 | fn test_extend_object() { 556 | match_and_assert( 557 | "extend type Foo @test { name: String! @skip }", 558 | vec!["@skip", "@test", "Foo", "String"], 559 | (Some(String::from("Foo__")), String::from("Foo")), 560 | GraphQL::TypeExtension(GraphQLType::Object), 561 | ); 562 | } 563 | 564 | #[test] 565 | fn test_scalar() { 566 | match_and_assert( 567 | "scalar Foo @test", 568 | vec!["@test"], 569 | (None, String::from("Foo")), 570 | GraphQL::TypeDefinition(GraphQLType::Scalar), 571 | ); 572 | } 573 | 574 | #[test] 575 | fn test_extend_scalar() { 576 | match_and_assert( 577 | "extend scalar Foo @test", 578 | vec!["@test", "Foo"], 579 | (Some(String::from("Foo__")), String::from("Foo")), 580 | GraphQL::TypeExtension(GraphQLType::Scalar), 581 | ); 582 | } 583 | 584 | #[test] 585 | fn test_union() { 586 | match_and_assert( 587 | "union Foo @test = A | B | C", 588 | vec!["@test", "A", "B", "C"], 589 | (None, String::from("Foo")), 590 | GraphQL::TypeDefinition(GraphQLType::Union), 591 | ); 592 | } 593 | 594 | #[test] 595 | fn test_extend_union() { 596 | match_and_assert( 597 | "extend union Foo @test = D", 598 | vec!["@test", "D", "Foo"], 599 | (Some(String::from("Foo__")), String::from("Foo")), 600 | GraphQL::TypeExtension(GraphQLType::Union), 601 | ); 602 | } 603 | 604 | #[test] 605 | fn test_directive() { 606 | match_and_assert( 607 | r#"directive @foo( reason: String = "Woot!" ) on FIELD_DEFINITION | ENUM_VALUE"#, 608 | vec!["String"], 609 | (None, String::from("@foo")), 610 | GraphQL::Directive, 611 | ); 612 | } 613 | 614 | #[test] 615 | fn test_schema() { 616 | match_and_assert( 617 | "schema { query: Query mutation: Mutation subscription: Subscription }", 618 | vec!["Mutation", "Query", "Subscription"], 619 | (None, String::from("schema")), 620 | GraphQL::Schema, 621 | ); 622 | } 623 | } 624 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_debug_implementations, missing_docs, unreachable_pub)] 2 | #![deny(unsafe_code, nonstandard_style)] 3 | 4 | //! This library provides all the necessary methods and shared state for the craftql binary. 5 | //! Not meant to be used on its own! Primarily made for integration testing. 6 | 7 | /// Main onfiguration. 8 | pub mod config; 9 | /// Trait providing extension methods for graphql_parser::schema. 10 | pub mod extend_types; 11 | /// Global state. 12 | pub mod state; 13 | /// Utilities consumed by the binary. 14 | pub mod utils; 15 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use async_std::{ 2 | path::PathBuf, 3 | sync::{Arc, Mutex}, 4 | }; 5 | use petgraph::{graph::NodeIndex, Graph}; 6 | use std::{collections::HashMap, fmt, str::FromStr}; 7 | 8 | /// Global state. 9 | #[derive(Debug)] 10 | pub struct State { 11 | /// Shared part of the state. 12 | pub shared: Data, 13 | } 14 | 15 | /// Core GraphQL types used for definitions and extensions. 16 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 17 | pub enum GraphQLType { 18 | /// Enum type. 19 | Enum, 20 | /// InputObject type. 21 | InputObject, 22 | /// Interface type. 23 | Interface, 24 | /// Object type. 25 | Object, 26 | /// Scalar type. 27 | Scalar, 28 | /// Union type. 29 | Union, 30 | } 31 | 32 | /// Derived and simplified from graphql_parser::schema enums. 33 | #[derive(Clone, PartialEq, Eq)] 34 | pub enum GraphQL { 35 | /// Directive type. 36 | Directive, 37 | /// Schema type. 38 | Schema, 39 | /// TypeDefinition type. 40 | TypeDefinition(T), 41 | /// TypeExtension type. 42 | TypeExtension(T), 43 | } 44 | 45 | impl fmt::Debug for GraphQL 46 | where 47 | T: fmt::Debug + Copy, 48 | { 49 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 50 | match *self { 51 | GraphQL::Directive => write!(f, "Directive"), 52 | GraphQL::Schema => write!(f, "Schema"), 53 | GraphQL::TypeDefinition(graphql_type) => write!(f, "{:?}", graphql_type), 54 | GraphQL::TypeExtension(graphql_type) => write!(f, "{:?} extension", graphql_type), 55 | } 56 | } 57 | } 58 | 59 | impl FromStr for GraphQL { 60 | type Err = String; 61 | 62 | fn from_str(s: &str) -> Result { 63 | match s { 64 | "directive" => Ok(GraphQL::Directive), 65 | "enum" => Ok(GraphQL::TypeDefinition(GraphQLType::Enum)), 66 | "enum_extension" => Ok(GraphQL::TypeExtension(GraphQLType::Enum)), 67 | "input_object" => Ok(GraphQL::TypeDefinition(GraphQLType::InputObject)), 68 | "input_object_extension" => Ok(GraphQL::TypeExtension(GraphQLType::InputObject)), 69 | "interface" => Ok(GraphQL::TypeDefinition(GraphQLType::Interface)), 70 | "interface_extension" => Ok(GraphQL::TypeExtension(GraphQLType::Interface)), 71 | "object" => Ok(GraphQL::TypeDefinition(GraphQLType::Object)), 72 | "object_extension" => Ok(GraphQL::TypeExtension(GraphQLType::Object)), 73 | "scalar" => Ok(GraphQL::TypeDefinition(GraphQLType::Scalar)), 74 | "scalar_extension" => Ok(GraphQL::TypeExtension(GraphQLType::Scalar)), 75 | "schema" => Ok(GraphQL::Schema), 76 | "union" => Ok(GraphQL::TypeDefinition(GraphQLType::Union)), 77 | "union_extension" => Ok(GraphQL::TypeExtension(GraphQLType::Union)), 78 | unknown => Err(format!(r#"Unknown GraphQL type provided "{}""#, unknown)), 79 | } 80 | } 81 | } 82 | 83 | /// Represents a GraphQL entity. 84 | #[derive(Clone)] 85 | pub struct Entity { 86 | /// Dependencies of an entity. 87 | pub dependencies: Vec, 88 | /// GraphQL type of the entity. 89 | pub graphql: GraphQL, 90 | /// Id of the entity. 91 | pub id: String, 92 | /// Name of the entity. 93 | pub name: String, 94 | /// Path of the entity. 95 | pub path: PathBuf, 96 | /// Raw representation of the entity. 97 | pub raw: String, 98 | } 99 | 100 | impl Entity { 101 | /// Method to create a new Entity. 102 | pub fn new( 103 | dependencies: Vec, 104 | graphql: GraphQL, 105 | id: Option, 106 | name: String, 107 | path: PathBuf, 108 | raw: String, 109 | ) -> Self { 110 | Entity { 111 | dependencies, 112 | graphql, 113 | // If no custom id is provided, use the name. 114 | id: match id { 115 | Some(id) => id, 116 | None => name.clone(), 117 | }, 118 | name, 119 | path, 120 | raw, 121 | } 122 | } 123 | } 124 | 125 | // Used in graph generation. 126 | impl fmt::Debug for Entity { 127 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 128 | if self.dependencies.is_empty() { 129 | write!(f, "{} ({:?})", self.name, self.graphql) 130 | } else { 131 | write!( 132 | f, 133 | "{} ({:?})\n\n[{}]", 134 | self.name, 135 | self.graphql, 136 | self.dependencies.join(", ") 137 | ) 138 | } 139 | } 140 | } 141 | 142 | // Used with flags like --node. 143 | impl fmt::Display for Entity { 144 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 145 | write!(f, "\n# {}\n{}", self.path.to_string_lossy(), self.raw) 146 | } 147 | } 148 | 149 | /// A Node containing an Entity and a unique id. 150 | pub struct Node { 151 | /// Node's entity. 152 | pub entity: Entity, 153 | /// Using the entity name as id is safe as it is unique. 154 | /// http://spec.graphql.org/draft/#sec-Schema 155 | pub id: String, 156 | } 157 | 158 | impl Node { 159 | /// Method to create a new Node. 160 | pub fn new(entity: Entity, id: String) -> Self { 161 | Node { entity, id } 162 | } 163 | } 164 | 165 | impl fmt::Debug for Node { 166 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 167 | write!(f, "{:?}", self.entity) 168 | } 169 | } 170 | 171 | /// Data holding the thread-safe mutexes. 172 | #[derive(Debug, Clone)] 173 | pub struct Data { 174 | /// Dependencies mutex. 175 | pub dependencies: Arc>>>, 176 | /// Files mutex. 177 | pub files: Arc>>, 178 | /// Graph mutex. 179 | pub graph: Arc>>, 180 | /// Missing definition mutex. 181 | pub missing_definitions: Arc>>>, 182 | } 183 | 184 | impl State { 185 | /// Method to create a new State. 186 | pub fn new() -> Self { 187 | State { 188 | shared: Data { 189 | dependencies: Arc::new(Mutex::new(HashMap::new())), 190 | files: Arc::new(Mutex::new(HashMap::new())), 191 | graph: Arc::new(Mutex::new(Graph::::new())), 192 | missing_definitions: Arc::new(Mutex::new(HashMap::new())), 193 | }, 194 | } 195 | } 196 | } 197 | 198 | impl Default for State { 199 | fn default() -> Self { 200 | State::new() 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::ALLOWED_EXTENSIONS, 3 | extend_types::ExtendType, 4 | state::{Entity, GraphQL, GraphQLType, Node}, 5 | }; 6 | 7 | use anyhow::Result; 8 | use async_std::{ 9 | fs, 10 | future::Future, 11 | path::{Path, PathBuf}, 12 | pin::Pin, 13 | prelude::*, 14 | sync::{Arc, Mutex}, 15 | }; 16 | use graphql_parser::{parse_schema, schema}; 17 | use petgraph::{graph::NodeIndex, Direction}; 18 | use std::{collections::HashMap, process::exit}; 19 | 20 | /// Check if a file extension is allowed. 21 | fn is_extension_allowed(extension: &str) -> bool { 22 | ALLOWED_EXTENSIONS.to_vec().contains(&extension) 23 | } 24 | 25 | /// Print missing definitions. 26 | pub async fn print_missing_definitions( 27 | graph: Arc>>, 28 | missing_definitions: Arc>>>, 29 | ) -> Result<()> { 30 | let graph = graph.lock().await; 31 | let missing_definitions = missing_definitions.lock().await; 32 | 33 | for (node_index, definitions) in missing_definitions.iter() { 34 | println!( 35 | "\n# {} {} not defined in:{}", 36 | definitions.join(", "), 37 | if definitions.len() == 1 { "is" } else { "are" }, 38 | graph.node_weight(*node_index).unwrap().entity, 39 | ); 40 | } 41 | 42 | Ok(()) 43 | } 44 | 45 | /// Find and return neighbors of a node. 46 | pub async fn find_neighbors( 47 | node: &str, 48 | graph: Arc>>, 49 | direction: Direction, 50 | ) -> Vec { 51 | let graph = graph.lock().await; 52 | 53 | match graph.node_indices().find(|index| graph[*index].id == node) { 54 | Some(index) => graph 55 | .neighbors_directed(index, direction) 56 | .map(|index| &graph.node_weight(index).unwrap().entity) 57 | .cloned() 58 | .collect::>(), 59 | None => vec![], 60 | } 61 | } 62 | 63 | /// Print orphan nodes. 64 | pub async fn find_and_print_neighbors( 65 | node: &str, 66 | graph: Arc>>, 67 | direction: Direction, 68 | ) -> Result<()> { 69 | let graph_clone = graph.clone(); 70 | 71 | // Ensure that the node exists! 72 | find_node(node, graph).await?; 73 | 74 | let dependencies = find_neighbors(node, graph_clone, direction).await; 75 | 76 | if dependencies.is_empty() { 77 | eprintln!("No dependencies found for node {}", node); 78 | exit(1); 79 | } 80 | 81 | for dependency in dependencies { 82 | println!("{}", dependency); 83 | } 84 | 85 | Ok(()) 86 | } 87 | 88 | /// Find and return orphan nodes. 89 | pub async fn find_orphans( 90 | graph: Arc>>, 91 | ) -> Vec { 92 | let graph = &graph.lock().await; 93 | let externals = graph.externals(Direction::Outgoing); 94 | let has_root_schema = graph 95 | .node_indices() 96 | .any(|index| graph[index].id == "schema"); 97 | 98 | externals 99 | .filter_map(|index| { 100 | let entity = graph.node_weight(index).unwrap().entity.clone(); 101 | 102 | match entity.graphql { 103 | // Skip root schema has it can't have outgoing edges. 104 | GraphQL::Schema => None, 105 | // Skip Mutation, Query and Subscription if no root schema is defined 106 | // as those nodes can't have outgoing edges. 107 | GraphQL::TypeDefinition(GraphQLType::Object) 108 | if (!has_root_schema 109 | && (entity.name == "Mutation" 110 | || entity.name == "Query" 111 | || entity.name == "Subscription")) => 112 | { 113 | None 114 | } 115 | _ => Some(entity), 116 | } 117 | }) 118 | .collect::>() 119 | } 120 | 121 | /// Print orphan nodes. 122 | pub async fn find_and_print_orphans( 123 | graph: Arc>>, 124 | ) -> Result<()> { 125 | let orphans = find_orphans(graph).await; 126 | 127 | if orphans.is_empty() { 128 | eprintln!("No orphan node found"); 129 | exit(1); 130 | } 131 | 132 | for orphan in orphans { 133 | println!("{}", orphan); 134 | } 135 | 136 | Ok(()) 137 | } 138 | 139 | /// Find a node by name, display it with syntax highlighting or exit. 140 | pub async fn find_node( 141 | node: &str, 142 | graph: Arc>>, 143 | ) -> Result<()> { 144 | let graph = graph.lock().await; 145 | 146 | match graph.node_indices().find(|index| graph[*index].id == node) { 147 | Some(index) => { 148 | let entity = &graph.node_weight(index).unwrap().entity; 149 | 150 | println!("{}", entity); 151 | 152 | Ok(()) 153 | } 154 | None => { 155 | eprintln!("Node {} not found", node); 156 | exit(1); 157 | } 158 | } 159 | } 160 | 161 | /// Recursively read directories and files for a given path. 162 | pub fn get_files( 163 | path: PathBuf, 164 | files: Arc>>, 165 | ) -> Pin>>> { 166 | // Use a hack to get async recursive calls working. 167 | Box::pin(async move { 168 | let thread_safe_path = Arc::new(path); 169 | let file_or_dir = fs::metadata(thread_safe_path.as_ref()).await?; 170 | let file_type = file_or_dir.file_type(); 171 | let extension = match Path::new(thread_safe_path.as_ref()).extension() { 172 | Some(extension) => extension.to_str().unwrap(), 173 | None => "", 174 | }; 175 | 176 | if file_type.is_file() { 177 | if is_extension_allowed(extension) { 178 | let contents = fs::read_to_string(thread_safe_path.as_ref()).await?; 179 | let mut files = files.lock().await; 180 | 181 | files.insert(thread_safe_path.as_ref().clone(), contents); 182 | } 183 | 184 | return Ok(()); 185 | } 186 | 187 | let mut dir = fs::read_dir(thread_safe_path.as_ref()).await?; 188 | 189 | while let Some(result) = dir.next().await { 190 | let entry: fs::DirEntry = result?; 191 | let inner_path = entry.path(); 192 | let inner_path_cloned = inner_path.clone(); 193 | let metadata = entry.clone().metadata().await?; 194 | let is_dir = metadata.is_dir(); 195 | let extension = match &inner_path.extension() { 196 | Some(extension) => extension.to_str().unwrap(), 197 | None => "", 198 | }; 199 | 200 | if !is_dir && is_extension_allowed(extension) { 201 | let contents = fs::read_to_string(inner_path).await?; 202 | let mut files = files.lock().await; 203 | 204 | files.insert(inner_path_cloned, contents); 205 | } else { 206 | get_files(inner_path, files.clone()).await?; 207 | } 208 | } 209 | 210 | Ok(()) 211 | }) 212 | } 213 | 214 | async fn add_node_and_dependencies( 215 | entity: impl ExtendType, 216 | filter: &[GraphQL], 217 | graph: Arc>>, 218 | dependencies: Arc>>>, 219 | file: &(PathBuf, String), 220 | ) -> Result<()> { 221 | // If a filter is provided and the mapped type of the entity is not part of 222 | // this filter, skip it. 223 | if !filter.is_empty() && !filter.to_vec().contains(&entity.get_mapped_type()) { 224 | return Ok(()); 225 | } 226 | 227 | let mut graph = graph.lock().await; 228 | 229 | let entity_dependencies = entity.get_dependencies(); 230 | let (id, name) = entity.get_id_and_name(); 231 | let new_entity = Entity::new( 232 | entity_dependencies.clone(), 233 | entity.get_mapped_type(), 234 | id, 235 | name, 236 | file.0.to_owned(), 237 | entity.get_raw(), 238 | ); 239 | let node_id = new_entity.id.clone(); 240 | let node_index = graph.add_node(Node::new(new_entity, node_id)); 241 | 242 | // Update dependencies. 243 | let mut dependencies = dependencies.lock().await; 244 | dependencies.insert(node_index, entity_dependencies); 245 | 246 | Ok(()) 247 | } 248 | 249 | /// Parse the files, generate an AST and walk it to populate the graph. 250 | pub async fn populate_graph_from_ast( 251 | dependencies: Arc>>>, 252 | files: Arc>>, 253 | filter: &[GraphQL], 254 | graph: Arc>>, 255 | missing_definitions: Arc>>>, 256 | ) -> Result<()> { 257 | let files = files.lock().await; 258 | 259 | // Populate the nodes first. 260 | for file in files.clone() { 261 | let ast = parse_schema::(file.1.as_str())?; 262 | 263 | // Reference: http://spec.graphql.org/draft/ 264 | for definition in ast.definitions { 265 | let graph = graph.clone(); 266 | let dependencies = dependencies.clone(); 267 | 268 | match definition { 269 | schema::Definition::TypeDefinition(type_definition) => { 270 | add_node_and_dependencies(type_definition, filter, graph, dependencies, &file) 271 | .await? 272 | } 273 | schema::Definition::TypeExtension(type_extension) => { 274 | add_node_and_dependencies(type_extension, filter, graph, dependencies, &file) 275 | .await? 276 | } 277 | schema::Definition::SchemaDefinition(schema_definition) => { 278 | add_node_and_dependencies(schema_definition, filter, graph, dependencies, &file) 279 | .await? 280 | } 281 | schema::Definition::DirectiveDefinition(directive_definition) => { 282 | add_node_and_dependencies( 283 | directive_definition, 284 | filter, 285 | graph, 286 | dependencies, 287 | &file, 288 | ) 289 | .await? 290 | } 291 | } 292 | } 293 | } 294 | 295 | // Populate the edges. 296 | let dependencies = &*dependencies.lock().await; 297 | 298 | for (node_index, inner_dependencies) in dependencies { 299 | let mut node_missing_definitions: Vec = vec![]; 300 | 301 | for dependency in inner_dependencies { 302 | let mut graph = graph.lock().await; 303 | 304 | match graph 305 | .node_indices() 306 | .find(|index| graph[*index].id == *dependency) 307 | { 308 | Some(index) => match &graph[*node_index].entity.graphql { 309 | // Reverse edge for extension types. 310 | GraphQL::TypeExtension(GraphQLType::Enum) 311 | | GraphQL::TypeExtension(GraphQLType::InputObject) 312 | | GraphQL::TypeExtension(GraphQLType::Interface) 313 | | GraphQL::TypeExtension(GraphQLType::Object) 314 | | GraphQL::TypeExtension(GraphQLType::Scalar) 315 | | GraphQL::TypeExtension(GraphQLType::Union) => { 316 | graph.update_edge(*node_index, index, (*node_index, index)); 317 | } 318 | _ => { 319 | graph.update_edge(index, *node_index, (index, *node_index)); 320 | } 321 | }, 322 | None => match dependency.as_str() { 323 | // Built-in Scalars, skip. 324 | "Boolean" | "Float" | "ID" | "Int" | "String" => {} 325 | // Keep track of possible missing definitions, should have been resolved at this point! 326 | _ => { 327 | node_missing_definitions.push(dependency.to_owned()); 328 | } 329 | }, 330 | } 331 | } 332 | 333 | if !node_missing_definitions.is_empty() { 334 | missing_definitions 335 | .lock() 336 | .await 337 | .insert(*node_index, node_missing_definitions); 338 | } 339 | } 340 | 341 | Ok(()) 342 | } 343 | 344 | #[cfg(test)] 345 | mod tests { 346 | use super::*; 347 | 348 | use crate::state::{Data, GraphQL, GraphQLType, State}; 349 | 350 | use async_std::task; 351 | use petgraph::graph::NodeIndex; 352 | 353 | async fn scaffold(files: Vec<(PathBuf, String)>, filters: &[GraphQL]) -> Data { 354 | let state = State::new(); 355 | let shared_data = state.shared; 356 | let shared_data_for_populate = shared_data.clone(); 357 | 358 | task::block_on(async { 359 | let mut shared_files = shared_data.files.lock().await; 360 | 361 | for (path, contents) in files { 362 | shared_files.insert(path, contents); 363 | } 364 | }); 365 | 366 | populate_graph_from_ast( 367 | shared_data_for_populate.dependencies, 368 | shared_data_for_populate.files, 369 | filters, 370 | shared_data_for_populate.graph, 371 | shared_data_for_populate.missing_definitions, 372 | ) 373 | .await 374 | .unwrap(); 375 | 376 | shared_data 377 | } 378 | 379 | #[async_std::test] 380 | async fn check_dependencies_and_graph() { 381 | let house_contents = "type House { price: Int! rooms: Int! @test owner: Owner! }"; 382 | let house_dependencies = vec!["@test", "Int", "Owner"]; 383 | let house_name = "House"; 384 | let house_path = "some_path/House.gql"; 385 | 386 | let owner_contents = "type Owner { name: String! }"; 387 | let owner_dependencies = vec!["String"]; 388 | let owner_name = "Owner"; 389 | let owner_path = "some_path/Owner.graphql"; 390 | 391 | let shared_data = scaffold( 392 | vec![ 393 | (PathBuf::from(house_path), String::from(house_contents)), 394 | (PathBuf::from(owner_path), String::from(owner_contents)), 395 | ], 396 | &[], 397 | ) 398 | .await; 399 | 400 | let dependencies = shared_data.dependencies.lock().await; 401 | 402 | assert_eq!(dependencies.len(), 2); 403 | 404 | // There no determined insertion order, assign dependencies lists based 405 | // on respective assumed lengths. 406 | let (current_owner_dependencies, current_house_dependencies, is_same_order) = 407 | if dependencies.get(&NodeIndex::new(0)).unwrap().len() == house_dependencies.len() { 408 | ( 409 | dependencies.get(&NodeIndex::new(1)).unwrap(), 410 | dependencies.get(&NodeIndex::new(0)).unwrap(), 411 | true, 412 | ) 413 | } else { 414 | ( 415 | dependencies.get(&NodeIndex::new(0)).unwrap(), 416 | dependencies.get(&NodeIndex::new(1)).unwrap(), 417 | false, 418 | ) 419 | }; 420 | 421 | // List of dependencies should match. 422 | assert_eq!(current_owner_dependencies, &owner_dependencies); 423 | assert_eq!(current_house_dependencies, &house_dependencies); 424 | 425 | // Graph should contains 2 nodes and 1 edge. 426 | let graph = shared_data.graph.lock().await; 427 | assert_eq!(graph.node_count(), 2); 428 | assert_eq!(graph.edge_count(), 1); 429 | 430 | // Check house. 431 | let house_node_index = if is_same_order { 432 | NodeIndex::new(0) 433 | } else { 434 | NodeIndex::new(1) 435 | }; 436 | let house = graph.node_weight(house_node_index).unwrap(); 437 | assert_eq!(house.id, String::from(house_name)); 438 | assert_eq!(house.entity.dependencies, house_dependencies); 439 | assert_eq!( 440 | house.entity.graphql, 441 | GraphQL::TypeDefinition(GraphQLType::Object) 442 | ); 443 | assert_eq!(house.entity.id, String::from(house_name)); 444 | assert_eq!(house.entity.name, String::from(house_name)); 445 | assert_eq!(house.entity.path, PathBuf::from(house_path)); 446 | // We need to parse and format the AST in order to get the same output! 447 | assert_eq!( 448 | house.entity.raw.to_string(), 449 | format!( 450 | "{}", 451 | parse_schema::(house_contents).unwrap().to_owned() 452 | ) 453 | ); 454 | 455 | // Check owner. 456 | let owner_node_index = if is_same_order { 457 | NodeIndex::new(1) 458 | } else { 459 | NodeIndex::new(0) 460 | }; 461 | let owner = graph.node_weight(owner_node_index).unwrap(); 462 | assert_eq!(owner.id, String::from(owner_name)); 463 | assert_eq!(owner.entity.dependencies, owner_dependencies); 464 | assert_eq!( 465 | owner.entity.graphql, 466 | GraphQL::TypeDefinition(GraphQLType::Object) 467 | ); 468 | assert_eq!(owner.entity.id, String::from(owner_name)); 469 | assert_eq!(owner.entity.name, String::from(owner_name)); 470 | assert_eq!(owner.entity.path, PathBuf::from(owner_path)); 471 | // We need to parse and format the AST in order to get the same output! 472 | assert_eq!( 473 | owner.entity.raw.to_string(), 474 | format!( 475 | "{}", 476 | parse_schema::(owner_contents).unwrap().to_owned() 477 | ) 478 | ); 479 | 480 | // Check the edges. Owner should be directed to House, not the other 481 | // way around! 482 | assert!(graph.contains_edge(owner_node_index, house_node_index)); 483 | assert!(!graph.contains_edge(house_node_index, owner_node_index)); 484 | } 485 | 486 | #[async_std::test] 487 | async fn check_orphans() { 488 | let shared_data = scaffold( 489 | vec![( 490 | PathBuf::from("some_path/Peer.gql"), 491 | String::from("type Peer { id: String! }"), 492 | )], 493 | &[], 494 | ) 495 | .await; 496 | 497 | task::block_on(async { 498 | let graph = shared_data.graph.lock().await; 499 | assert_eq!(graph.node_count(), 1); 500 | assert_eq!(graph.edge_count(), 0); 501 | }); 502 | 503 | assert_eq!(find_orphans(shared_data.graph).await.len(), 1); 504 | } 505 | 506 | #[async_std::test] 507 | async fn check_orphans_with_schema() { 508 | let shared_data = scaffold( 509 | vec![( 510 | PathBuf::from("some_path/Schema.gql"), 511 | String::from("schema { query: Query }"), 512 | )], 513 | &[], 514 | ) 515 | .await; 516 | 517 | task::block_on(async { 518 | let graph = shared_data.graph.lock().await; 519 | assert_eq!(graph.node_count(), 1); 520 | assert_eq!(graph.edge_count(), 0); 521 | }); 522 | 523 | assert_eq!(find_orphans(shared_data.graph).await.len(), 0); 524 | } 525 | 526 | #[async_std::test] 527 | async fn check_orphans_without_schema_but_with_query_mutation_subscription() { 528 | let shared_data = scaffold( 529 | vec![ 530 | ( 531 | PathBuf::from("some_path/Query.gql"), 532 | String::from("type Query { foo(bar: String): String }"), 533 | ), 534 | ( 535 | PathBuf::from("some_path/Mutation.gql"), 536 | String::from("type Mutation { foo(bar: String): String }"), 537 | ), 538 | ( 539 | PathBuf::from("some_path/Subscription.gql"), 540 | String::from("type Subscription { foo(bar: String): String }"), 541 | ), 542 | ], 543 | &[], 544 | ) 545 | .await; 546 | 547 | task::block_on(async { 548 | let graph = shared_data.graph.lock().await; 549 | assert_eq!(graph.node_count(), 3); 550 | assert_eq!(graph.edge_count(), 0); 551 | }); 552 | 553 | assert_eq!(find_orphans(shared_data.graph).await.len(), 0); 554 | } 555 | 556 | #[async_std::test] 557 | async fn check_neighbors() { 558 | let shared_data = scaffold( 559 | vec![ 560 | ( 561 | PathBuf::from("some_path/Foo.gql"), 562 | String::from("type Foo { field: Bar }"), 563 | ), 564 | ( 565 | PathBuf::from("some_path/Bar.gql"), 566 | String::from("interface Bar { id: ID!}"), 567 | ), 568 | ], 569 | &[], 570 | ) 571 | .await; 572 | 573 | task::block_on(async { 574 | let graph = shared_data.graph.lock().await; 575 | assert_eq!(graph.node_count(), 2); 576 | assert_eq!(graph.edge_count(), 1); 577 | }); 578 | 579 | // Foo depends on Bar but is not a dependency. 580 | let incoming = find_neighbors("Foo", shared_data.graph.clone(), Direction::Incoming).await; 581 | let outgoing = find_neighbors("Foo", shared_data.graph.clone(), Direction::Outgoing).await; 582 | 583 | assert_eq!(incoming.len(), 1); 584 | assert_eq!(incoming.first().unwrap().name, "Bar"); 585 | assert_eq!(outgoing.len(), 0); 586 | 587 | // Bar depends on nothing but is a dependency of Foo. 588 | let incoming = find_neighbors("Bar", shared_data.graph.clone(), Direction::Incoming).await; 589 | let outgoing = find_neighbors("Bar", shared_data.graph.clone(), Direction::Outgoing).await; 590 | 591 | assert_eq!(incoming.len(), 0); 592 | assert_eq!(outgoing.len(), 1); 593 | assert_eq!(outgoing.first().unwrap().name, "Foo"); 594 | } 595 | 596 | #[async_std::test] 597 | async fn check_missing_definitions() { 598 | let shared_data = scaffold( 599 | vec![ 600 | ( 601 | PathBuf::from("some_path/Foo.gql"), 602 | String::from("type Foo { field: Woot, otherField: Why }"), 603 | ), 604 | ( 605 | PathBuf::from("some_path/Bar.gql"), 606 | String::from("interface Bar { id: What!}"), 607 | ), 608 | ], 609 | &[], 610 | ) 611 | .await; 612 | 613 | let graph = shared_data.graph.lock().await; 614 | assert_eq!(graph.node_count(), 2); 615 | assert_eq!(graph.edge_count(), 0); 616 | 617 | let missing_definitions = shared_data.missing_definitions.lock().await; 618 | 619 | let (bar_missing_dependencies, foo_missing_dependencies) = 620 | if missing_definitions.get(&NodeIndex::new(0)).unwrap().len() == 2 { 621 | ( 622 | missing_definitions.get(&NodeIndex::new(1)).unwrap(), 623 | missing_definitions.get(&NodeIndex::new(0)).unwrap(), 624 | ) 625 | } else { 626 | ( 627 | missing_definitions.get(&NodeIndex::new(0)).unwrap(), 628 | missing_definitions.get(&NodeIndex::new(1)).unwrap(), 629 | ) 630 | }; 631 | 632 | assert_eq!( 633 | *foo_missing_dependencies, 634 | vec![String::from("Why"), String::from("Woot")] 635 | ); 636 | assert_eq!(*bar_missing_dependencies, vec![String::from("What")]); 637 | } 638 | 639 | #[async_std::test] 640 | async fn check_filtering() { 641 | let shared_data = scaffold( 642 | vec![ 643 | ( 644 | PathBuf::from("some_path/Foo.gql"), 645 | String::from("type Foo { a: ID! }"), 646 | ), 647 | ( 648 | PathBuf::from("some_path/Bar.gql"), 649 | String::from("interface Bar { b: ID! }"), 650 | ), 651 | ( 652 | PathBuf::from("some_path/Cow.gql"), 653 | String::from("input Cow { c: ID! }"), 654 | ), 655 | ], 656 | &[ 657 | GraphQL::TypeDefinition(GraphQLType::Object), 658 | GraphQL::TypeDefinition(GraphQLType::InputObject), 659 | ], 660 | ) 661 | .await; 662 | 663 | let graph = shared_data.graph.lock().await; 664 | assert_eq!(graph.node_count(), 2); 665 | assert_eq!(graph.edge_count(), 0); 666 | 667 | let mut selected_entities = graph 668 | .node_indices() 669 | .map(|index| &graph.node_weight(index).unwrap().id) 670 | .collect::>(); 671 | // Sort the entities as the order is not as there no determined insertion order. 672 | selected_entities.sort(); 673 | assert_eq!(selected_entities, vec!["Cow", "Foo"]); 674 | } 675 | } 676 | -------------------------------------------------------------------------------- /tests/fixtures/Directives/deprecated.graphql: -------------------------------------------------------------------------------- 1 | directive @deprecated( 2 | reason: String = "No longer supported" 3 | ) on FIELD_DEFINITION | ENUM_VALUE 4 | -------------------------------------------------------------------------------- /tests/fixtures/Directives/test.graphql: -------------------------------------------------------------------------------- 1 | directive @test( 2 | letter: Letter = A 3 | ) on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION 4 | -------------------------------------------------------------------------------- /tests/fixtures/Types/Enums/Episode.gql: -------------------------------------------------------------------------------- 1 | # The episodes in the Star Wars trilogy 2 | enum Episode @test(letter: B) { 3 | # Star Wars Episode IV: A New Hope, released in 1977. 4 | NEWHOPE @deprecated 5 | # Star Wars Episode V: The Empire Strikes Back, released in 1980. 6 | EMPIRE 7 | # Star Wars Episode VI: Return of the Jedi, released in 1983. 8 | JEDI 9 | } 10 | -------------------------------------------------------------------------------- /tests/fixtures/Types/Enums/EpisodeExtension.graphql: -------------------------------------------------------------------------------- 1 | extend enum Episode { 2 | # Star Wars Episode I: The Phantom Menace, released in 1999. 3 | PHANTOM 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/Types/Enums/LengthUnit.graphql: -------------------------------------------------------------------------------- 1 | # Units of height 2 | enum LengthUnit { 3 | # The standard unit around the world 4 | METER 5 | # Primarily used in the United States 6 | FOOT 7 | } 8 | -------------------------------------------------------------------------------- /tests/fixtures/Types/Enums/Letter.gql: -------------------------------------------------------------------------------- 1 | enum Letter { 2 | A 3 | B 4 | C 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixtures/Types/Inputs/a.gql: -------------------------------------------------------------------------------- 1 | # The input object sent when someone is creating a new review 2 | input ReviewInput { 3 | # 0-5 stars 4 | stars: Int! 5 | # Comment about the movie, optional 6 | commentary: String 7 | # Favorite color, optional 8 | favorite_color: ColorInput 9 | } 10 | # The input object sent when passing in a color 11 | input ColorInput @test { 12 | red: Int! 13 | green: Int! 14 | blue: Int! 15 | cyan: Int! @deprecated 16 | } 17 | -------------------------------------------------------------------------------- /tests/fixtures/Types/Inputs/extension.gql: -------------------------------------------------------------------------------- 1 | extend input ColorInput { 2 | alpha: Int! 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/Types/Interfaces/Character.graphql: -------------------------------------------------------------------------------- 1 | # A character from the Star Wars universe 2 | interface Character @test { 3 | # The ID of the character 4 | id: ID! 5 | # The name of the character 6 | name: String! 7 | # The friends of the character, or an empty list if they have none 8 | friends: [Character] 9 | # The friends of the character exposed as a connection with edges 10 | friendsConnection(first: Int, after: ID): FriendsConnection! 11 | # The movies this character appears in 12 | appearsIn: [Episode]! 13 | # Well.. 14 | cute: Boolean! @deprecated 15 | # Missing definition 16 | preferedColor: Color 17 | } 18 | -------------------------------------------------------------------------------- /tests/fixtures/Types/Interfaces/CharacterExtension.gql: -------------------------------------------------------------------------------- 1 | extend interface Character { 2 | # Is the character friendly? 3 | friendly: Boolean! 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/Types/Scalars/DateTime.graphql: -------------------------------------------------------------------------------- 1 | scalar DateTime -------------------------------------------------------------------------------- /tests/fixtures/Types/Scalars/DateTimeExtension.gql: -------------------------------------------------------------------------------- 1 | extend scalar DateTime @test 2 | -------------------------------------------------------------------------------- /tests/fixtures/Types/Types/a.gql: -------------------------------------------------------------------------------- 1 | # The query type, represents all of the entry points into our object graph 2 | type Query { 3 | hero(episode: Episode): Character 4 | reviews(episode: Episode!): [Review] 5 | search(text: String): [SearchResult] 6 | character(id: ID!): Character 7 | droid(id: ID!): Droid 8 | human(id: ID!): Human 9 | starship(id: ID!): Starship 10 | } 11 | # The mutation type, represents all updates we can make to our data 12 | type Mutation { 13 | createReview(episode: Episode, review: ReviewInput!): Review 14 | } 15 | # The subscription type, represents all subscriptions we can make to our data 16 | type Subscription { 17 | reviewAdded(episode: Episode): Review 18 | } 19 | -------------------------------------------------------------------------------- /tests/fixtures/Types/Types/b.graphql: -------------------------------------------------------------------------------- 1 | # A humanoid creature from the Star Wars universe 2 | type Human implements Character { 3 | # The ID of the human 4 | id: ID! 5 | # What this human calls themselves 6 | name: String! 7 | # The home planet of the human, or null if unknown 8 | homePlanet: String 9 | # Height in the preferred unit, default is meters 10 | height(unit: LengthUnit = METER): Float 11 | # Mass in kilograms, or null if unknown 12 | mass: Float 13 | # This human's friends, or an empty list if they have none 14 | friends: [Character] 15 | # The friends of the human exposed as a connection with edges 16 | friendsConnection(first: Int, after: ID): FriendsConnection! 17 | # The movies this human appears in 18 | appearsIn: [Episode]! 19 | # A list of starships this person has piloted, or an empty list if none 20 | starships: [Starship] 21 | } 22 | # An autonomous mechanical character in the Star Wars universe 23 | type Droid implements Character { 24 | # The ID of the droid 25 | id: ID! 26 | # What others call this droid 27 | name: String! 28 | # This droid's friends, or an empty list if they have none 29 | friends: [Character] 30 | # The friends of the droid exposed as a connection with edges 31 | friendsConnection(first: Int, after: ID): FriendsConnection! 32 | # The movies this droid appears in 33 | appearsIn: [Episode]! 34 | # This droid's primary function 35 | primaryFunction: String 36 | } 37 | # A connection object for a character's friends 38 | type FriendsConnection { 39 | # The total number of friends 40 | totalCount: Int 41 | # The edges for each of the character's friends. 42 | edges: [FriendsEdge] 43 | # A list of the friends, as a convenience when edges are not needed. 44 | friends: [Character] 45 | # Information for paginating this connection 46 | pageInfo: PageInfo! 47 | } 48 | # An edge object for a character's friends 49 | type FriendsEdge { 50 | # A cursor used for pagination 51 | cursor: ID! 52 | # The character represented by this friendship edge 53 | node: Character 54 | } 55 | # Information for paginating this connection 56 | type PageInfo @test { 57 | startCursor: ID 58 | endCursor: ID 59 | hasNextPage: Boolean! 60 | } 61 | # Represents a review for a movie 62 | type Review { 63 | # The movie 64 | episode: Episode 65 | # The number of stars this review gave, 1-5 66 | stars: Int! @test 67 | # Comment about the movie 68 | commentary: String 69 | # Creation date 70 | createdAt: DateTime 71 | } 72 | -------------------------------------------------------------------------------- /tests/fixtures/Types/Types/c.gql: -------------------------------------------------------------------------------- 1 | type Starship { 2 | # The ID of the starship 3 | id: ID! 4 | # The name of the starship 5 | newName: String! 6 | oldName: String! @deprecated(reason: "Use `newName`.") 7 | # Length of the starship, along the longest axis 8 | length(unit: LengthUnit = METER): Float 9 | coordinates: [[Float!]!] 10 | } -------------------------------------------------------------------------------- /tests/fixtures/Types/Types/extension.graphql: -------------------------------------------------------------------------------- 1 | extend type Starship { 2 | antiGravity: Boolean! 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/Types/Types/orphan.gql: -------------------------------------------------------------------------------- 1 | type Orphan { 2 | id: ID! 3 | } 4 | -------------------------------------------------------------------------------- /tests/fixtures/Types/Unions/SearchResultExtension.graphql: -------------------------------------------------------------------------------- 1 | extend union SearchResult = Ewok | Gungan -------------------------------------------------------------------------------- /tests/fixtures/Types/Unions/SearchResults.graphql: -------------------------------------------------------------------------------- 1 | union SearchResult @test = Human | Droid | Starship 2 | -------------------------------------------------------------------------------- /tests/fixtures/c.txt: -------------------------------------------------------------------------------- 1 | foobar -------------------------------------------------------------------------------- /tests/fixtures/schema.graphql: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Query 3 | mutation: Mutation 4 | subscription: Subscription 5 | } 6 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | extern crate craftql; 2 | 3 | use anyhow::Result; 4 | use async_std::{fs, path::PathBuf}; 5 | use craftql::{state::State, utils::get_files}; 6 | 7 | #[async_std::test] 8 | async fn check_get_files() -> Result<()> { 9 | let state = State::default(); 10 | let shared_data = state.shared; 11 | let shared_data_cloned = shared_data.clone(); 12 | 13 | get_files(PathBuf::from("./tests/fixtures"), shared_data.files).await?; 14 | 15 | let files = shared_data_cloned.files.lock().await; 16 | 17 | assert_eq!(files.len(), 20); 18 | 19 | let contents = fs::read_to_string("./tests/fixtures/Types/Enums/Episode.gql").await?; 20 | 21 | assert_eq!( 22 | files.get(&PathBuf::from("./tests/fixtures/Types/Enums/Episode.gql")), 23 | Some(&contents) 24 | ); 25 | 26 | Ok(()) 27 | } 28 | --------------------------------------------------------------------------------